From 7c8daacc6506f709d050f27dd20c6321f62b16e8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 11:48:19 +0000 Subject: [PATCH 1/4] Reorganize repository, add CI and presets, fix Type assignment use-after-free - Add CMakePresets.json, GitHub Actions CI, issue/PR templates, CONTRIBUTING, .editorconfig and .gitattributes - Move ARCHITECTURE.md and BUILD.md to docs/; move test registration into tests/CMakeLists.txt - Add UINX_WARNINGS_AS_ERRORS and UINX_ENABLE_SANITIZERS options - Remove stale MANIFEST.sha256 (35 of 159 entries no longer matched) and the checked-in verification transcripts that contradicted their own README - Fix heap-use-after-free in Type::operator= when assigning from an owned pointee (found by ASan), and a -Wconversion warning in codegen - Apply clang-format to files that had drifted Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_018uUuRXQcBduAQvVUcJYkWz --- .editorconfig | 21 ++++ .gitattributes | 2 + .github/ISSUE_TEMPLATE/bug_report.md | 18 +++ .github/ISSUE_TEMPLATE/config.yml | 5 + .github/PULL_REQUEST_TEMPLATE.md | 10 ++ .github/workflows/ci.yml | 46 +++++++ .gitignore | 10 ++ CHANGELOG.md | 2 + CMakeLists.txt | 40 +++--- CMakePresets.json | 68 ++++++++++ CONTRIBUTING.md | 56 +++++++++ MANIFEST.sha256 | 159 ------------------------ README.md | 3 + ARCHITECTURE.md => docs/ARCHITECTURE.md | 0 BUILD.md => docs/BUILD.md | 14 +++ include/uinx/mir.hpp | 7 +- include/uinx/types.hpp | 11 ++ src/codegen.cpp | 2 +- src/mir.cpp | 8 +- tests/CMakeLists.txt | 28 +++++ tools/uinx.cpp | 15 +-- verification/README.md | 5 - verification/compiler-benchmark.txt | 3 - verification/ctest-release.txt | 42 ------- verification/environment.txt | 11 -- verification/examples-check.txt | 25 ---- verification/optimizer-check.txt | 6 - verification/os-freestanding.txt | 11 -- verification/release-build.txt | 6 - verification/stdlib-check.txt | 2 - 30 files changed, 320 insertions(+), 316 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/ci.yml create mode 100644 CMakePresets.json create mode 100644 CONTRIBUTING.md delete mode 100644 MANIFEST.sha256 rename ARCHITECTURE.md => docs/ARCHITECTURE.md (100%) rename BUILD.md => docs/BUILD.md (86%) create mode 100644 tests/CMakeLists.txt delete mode 100644 verification/README.md delete mode 100644 verification/compiler-benchmark.txt delete mode 100644 verification/ctest-release.txt delete mode 100644 verification/environment.txt delete mode 100644 verification/examples-check.txt delete mode 100644 verification/optimizer-check.txt delete mode 100644 verification/os-freestanding.txt delete mode 100644 verification/release-build.txt delete mode 100644 verification/stdlib-check.txt diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d30ad2f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.{json,yml,yaml}] +indent_size = 2 + +[CMakeLists.txt] +indent_size = 2 + +[*.cmake] +indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d0d6371 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto eol=lf +*.png binary diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..b8b569a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,18 @@ +--- +name: Bug report +about: The compiler, runtime or tooling misbehaves +labels: bug +--- + +**Uinx version / commit** + +**Host and target triple** + +**Smallest reproducing source** + +```uinx +``` + +**Command line** + +**Expected vs. actual behavior** diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..deb81dc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Security report + url: https://github.com/ViudiraTech/Uinx/blob/main/SECURITY.md + about: Compiler crashes, borrow-checker soundness failures and memory-safety escapes diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..1ae9643 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,10 @@ +## Summary + + + +## Checklist + +- [ ] `cmake --preset ci && cmake --build --preset ci && ctest --preset ci` passes +- [ ] `clang-format` is clean (`cmake --build --target format`) +- [ ] New language behavior has a test under `tests/` (`conformance/`, `safety/`, `codegen/` or `runtime/`) +- [ ] User-visible changes are recorded in `CHANGELOG.md` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..434d300 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-test: + name: ${{ matrix.compiler }} / ${{ matrix.preset }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + include: + - { compiler: gcc, cc: gcc, cxx: g++, preset: ci } + - { compiler: clang, cc: clang, cxx: clang++, preset: ci } + - { compiler: clang, cc: clang, cxx: clang++, preset: asan } + steps: + - uses: actions/checkout@v4 + - name: Install toolchain + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends ninja-build clang lld + - name: Configure + run: cmake --preset ${{ matrix.preset }} -DCMAKE_C_COMPILER=${{ matrix.cc }} -DCMAKE_CXX_COMPILER=${{ matrix.cxx }} + - name: Build + run: cmake --build --preset ${{ matrix.preset }} + - name: Test + run: ctest --preset ${{ matrix.preset }} + + format: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Install clang-format + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends clang-format + - name: Check formatting + run: git ls-files '*.cpp' '*.hpp' '*.c' '*.h' | xargs clang-format --dry-run --Werror diff --git a/.gitignore b/.gitignore index 8c28f11..799a4e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,19 @@ +# Build trees /build/ /build-*/ /Testing/ +CMakeUserPresets.json +compile_commands.json + +# Uinx outputs .uinx/ target/ *.o *.ll *.elf + +# Editor / tooling *.vsix +.vscode/ +.idea/ +.cache/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 6919d11..28a5f87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ **Copyright © 2026 ViudiraTech · Code by JiTianYu391** +- Fixed a heap-use-after-free in `uinx::Type` copy assignment (`t = *t.pointee`) found by AddressSanitizer. +- Repository hygiene: added `CMakePresets.json`, GitHub Actions CI (GCC, Clang, ASan/UBSan, clang-format), issue/PR templates, `CONTRIBUTING.md`, `.editorconfig` and `.gitattributes`; moved `ARCHITECTURE.md` and `BUILD.md` into `docs/`; moved test registration into `tests/CMakeLists.txt`; added `UINX_WARNINGS_AS_ERRORS` and `UINX_ENABLE_SANITIZERS` options; removed the stale `MANIFEST.sha256` and checked-in `verification/` transcripts. - Reworked borrow checking around backward liveness plus forward control-flow fixed points instead of block-local statement numbering. - Added HIR `SymbolId`-based place identity so shadowed locals cannot erase or inherit another binding's move/loan state. - Added partial move/reinitialization tracking, branch/loop joins, break/continue/return reachability handling, field-level reference provenance, method-receiver provenance, aggregate escape checking, and conservative async suspension checks. diff --git a/CMakeLists.txt b/CMakeLists.txt index 326918b..94dc470 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,11 +12,26 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) option(UINX_BUILD_TESTS "Build Uinx compiler tests" ON) option(UINX_BUILD_FUZZ "Build libFuzzer targets when supported" OFF) +option(UINX_WARNINGS_AS_ERRORS "Treat compiler warnings as errors" OFF) +option(UINX_ENABLE_SANITIZERS "Build with AddressSanitizer and UndefinedBehaviorSanitizer" OFF) if(MSVC) add_compile_options(/W4 /permissive-) + if(UINX_WARNINGS_AS_ERRORS) + add_compile_options(/WX) + endif() else() add_compile_options(-Wall -Wextra -Wpedantic -Wconversion -Wshadow) + if(UINX_WARNINGS_AS_ERRORS) + add_compile_options(-Werror) + endif() + if(UINX_ENABLE_SANITIZERS) + # C++ only: the C runtime is linked into programs built by uinxc, which are + # not sanitized. + add_compile_options($<$:-fsanitize=address,undefined> + $<$:-fno-omit-frame-pointer>) + add_link_options(-fsanitize=address,undefined) + endif() endif() add_library(uinx_compiler @@ -76,30 +91,7 @@ endif() if(UINX_BUILD_TESTS) enable_testing() - add_executable(uinx-unit-tests tests/unit/compiler_unit.cpp) - target_link_libraries(uinx-unit-tests PRIVATE uinx_compiler) - add_test(NAME unit.compiler COMMAND uinx-unit-tests) - add_executable(uinx-runtime-tests tests/unit/runtime_unit.c) - target_link_libraries(uinx-runtime-tests PRIVATE uinx_runtime) - add_test(NAME unit.runtime COMMAND uinx-runtime-tests) - add_test(NAME codegen.asm-object COMMAND $ ${CMAKE_CURRENT_SOURCE_DIR}/tests/codegen/asm/x86_64_nop.ux --emit=obj -o ${CMAKE_CURRENT_BINARY_DIR}/asm-test.o) - add_test(NAME codegen.asm-input-output COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DSOURCE=${CMAKE_CURRENT_SOURCE_DIR}/tests/codegen/asm/x86_64_io.ux -DOUTPUT=${CMAKE_CURRENT_BINARY_DIR}/asm-io -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/RunExecutableTest.cmake) - add_test(NAME codegen.asm-inout COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DSOURCE=${CMAKE_CURRENT_SOURCE_DIR}/tests/codegen/asm/x86_64_inout.ux -DOUTPUT=${CMAKE_CURRENT_BINARY_DIR}/asm-inout -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/RunExecutableTest.cmake) - add_test(NAME codegen.asm-multiple-outputs COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DSOURCE=${CMAKE_CURRENT_SOURCE_DIR}/tests/codegen/asm/x86_64_multiout.ux -DOUTPUT=${CMAKE_CURRENT_BINARY_DIR}/asm-multiout -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/RunExecutableTest.cmake) - add_test(NAME runtime.slice-index-bounds COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DSOURCE=${CMAKE_CURRENT_SOURCE_DIR}/tests/conformance/pass/slice_index.ux -DOUTPUT=${CMAKE_CURRENT_BINARY_DIR}/slice-index -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/RunExecutableTest.cmake) - add_test(NAME runtime.slice-index-oob-traps COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DSOURCE=${CMAKE_CURRENT_SOURCE_DIR}/tests/conformance/pass/slice_oob.ux -DOUTPUT=${CMAKE_CURRENT_BINARY_DIR}/slice-oob -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/RunExpectedRuntimeFailure.cmake) - add_test(NAME runtime.c-ffi COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DSOURCE=${CMAKE_CURRENT_SOURCE_DIR}/examples/c_ffi.ux -DOUTPUT=${CMAKE_CURRENT_BINARY_DIR}/c-ffi -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/RunExecutableTest.cmake) - add_test(NAME headers.enabled COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DSOURCE=${CMAKE_CURRENT_SOURCE_DIR}/tests/headers/main.ux -DINCLUDE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/headers/include -DOUTPUT=${CMAKE_CURRENT_BINARY_DIR}/header-test -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/RunHeaderTest.cmake) - add_test(NAME codegen.cross-target-objects COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DSOURCE=${CMAKE_CURRENT_SOURCE_DIR}/examples/freestanding.ux -DBINARY_DIR=${CMAKE_CURRENT_BINARY_DIR} -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/RunCrossTargetTests.cmake) - add_test(NAME codegen.asm-cross-targets COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DSOURCE=${CMAKE_CURRENT_SOURCE_DIR}/tests/codegen/asm/cross_nop.ux -DBINARY_DIR=${CMAKE_CURRENT_BINARY_DIR} -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/RunAsmCrossTargets.cmake) - add_test(NAME codegen.asm-invalid-target-register COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DSOURCE=${CMAKE_CURRENT_SOURCE_DIR}/tests/codegen/asm/invalid_aarch64_reg.ux -DOUTPUT=${CMAKE_CURRENT_BINARY_DIR}/asm-invalid.o -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/RunInvalidAsmConstraint.cmake) - add_test(NAME codegen.baremetal-cross-link COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DCLANG=clang -DLD_LLD=ld.lld -DSOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/examples/baremetal -DBINARY_DIR=${CMAKE_CURRENT_BINARY_DIR} -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/RunBareMetalTests.cmake) - add_test(NAME runtime.async-suspend-resume COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DSOURCE=${CMAKE_CURRENT_SOURCE_DIR}/tests/runtime/async/suspend_resume.ux -DUINX_OBJ=${CMAKE_CURRENT_BINARY_DIR}/async-test.o -DCLANG=clang -DRUNTIME_INCLUDE=${CMAKE_CURRENT_SOURCE_DIR}/runtime -DHARNESS=${CMAKE_CURRENT_SOURCE_DIR}/tests/support/async_harness.c -DRUNTIME_LIB=$ -DOUTPUT=${CMAKE_CURRENT_BINARY_DIR}/async-test -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/RunAsyncTest.cmake) - add_test(NAME conformance.positive COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DTEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/conformance/pass -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/RunPassTests.cmake) - add_test(NAME conformance.negative COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DTEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/conformance/fail -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/RunFailTests.cmake) - add_test(NAME safety.borrow-negative COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DTEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/safety/borrow-fail -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/RunFailTests.cmake) - add_test(NAME safety.smp-negative COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DTEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/safety/smp-fail -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/RunFailTests.cmake) - add_test(NAME tooling.package-integration COMMAND ${CMAKE_COMMAND} -DUINX=$ -DWORK=${CMAKE_CURRENT_BINARY_DIR}/package-tooling -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/RunPackageToolingTest.cmake) + add_subdirectory(tests) endif() if(UINX_BUILD_FUZZ AND CMAKE_CXX_COMPILER_ID MATCHES "Clang") diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..be10e4e --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,68 @@ +{ + "version": 6, + "cmakeMinimumRequired": { "major": 3, "minor": 25, "patch": 0 }, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}" + }, + { + "name": "debug", + "inherits": "base", + "displayName": "Debug", + "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } + }, + { + "name": "release", + "inherits": "base", + "displayName": "Release", + "cacheVariables": { "CMAKE_BUILD_TYPE": "Release" } + }, + { + "name": "ci", + "inherits": "release", + "displayName": "CI (Release, warnings as errors)", + "cacheVariables": { "UINX_WARNINGS_AS_ERRORS": "ON" } + }, + { + "name": "asan", + "inherits": "base", + "displayName": "Debug + ASan/UBSan", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "UINX_ENABLE_SANITIZERS": "ON" + } + }, + { + "name": "fuzz", + "inherits": "release", + "displayName": "Clang + libFuzzer", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++", + "UINX_BUILD_FUZZ": "ON" + } + } + ], + "buildPresets": [ + { "name": "debug", "configurePreset": "debug" }, + { "name": "release", "configurePreset": "release" }, + { "name": "ci", "configurePreset": "ci" }, + { "name": "asan", "configurePreset": "asan" }, + { "name": "fuzz", "configurePreset": "fuzz" } + ], + "testPresets": [ + { + "name": "default", + "hidden": true, + "output": { "outputOnFailure": true } + }, + { "name": "debug", "inherits": "default", "configurePreset": "debug" }, + { "name": "release", "inherits": "default", "configurePreset": "release" }, + { "name": "ci", "inherits": "default", "configurePreset": "ci" }, + { "name": "asan", "inherits": "default", "configurePreset": "asan" }, + { "name": "fuzz", "inherits": "default", "configurePreset": "fuzz" } + ] +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3e59d1c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,56 @@ +# Contributing to Uinx + +Thanks for helping out. Security-relevant reports (compiler crashes, +borrow-checker soundness failures, safe-language memory-safety escapes) should +follow [SECURITY.md](SECURITY.md) instead of the public issue tracker. + +## Build and test + +```sh +cmake --preset ci +cmake --build --preset ci +ctest --preset ci +``` + +Other presets: `debug`, `release`, `asan` (ASan + UBSan), `fuzz` (Clang + +libFuzzer). Toolchain requirements are listed in [docs/BUILD.md](docs/BUILD.md). + +## Repository layout + +| Path | Contents | +|---|---| +| `include/uinx/` | public compiler headers | +| `src/` | compiler front end, borrow checker, MIR, LLVM IR backend | +| `tools/` | `uinx`, `uinxc`, `uinx-lsp` executables | +| `runtime/` | C runtime linked into hosted programs | +| `stdlib/` | standard library in Uinx | +| `tests/` | unit, conformance, safety, codegen and runtime tests | +| `cmake/` | CTest driver scripts (`Run*.cmake`) | +| `examples/` | sample programs, also exercised by tests | +| `fuzz/`, `benchmarks/` | libFuzzer targets and compiler benchmark | +| `editors/` | editor integrations (VS Code) | +| `docs/` | specification, architecture, build and design documents | + +## Tests + +Every language-level change needs a test in `tests/`; see +[tests/README.md](tests/README.md). Negative tests must carry the stable +`EXPECT-DIAGNOSTIC` code they are meant to hit. + +## Style + +C and C++ are formatted with `clang-format` using the checked-in `.clang-format`: + +```sh +cmake --build build/ci --target format +``` + +CI runs `clang-format --dry-run --Werror` and builds with warnings as errors. + +## Commits and pull requests + +Use short imperative subjects, preferably `area: change` (for example +`borrowck: reject dangling field reference`). Record user-visible changes in +[CHANGELOG.md](CHANGELOG.md). + +Build outputs, logs and benchmark transcripts are not committed. diff --git a/MANIFEST.sha256 b/MANIFEST.sha256 deleted file mode 100644 index cbd4b29..0000000 --- a/MANIFEST.sha256 +++ /dev/null @@ -1,159 +0,0 @@ -1f159c7a91d45246e64fd7ab1a33f812427934ae7f3677fef11dc9885b1486d2 ./ARCHITECTURE.md -9203c45e4c1f7fe10d0042f76b3e7b13d99964ec3f776ff3ee9f5eb095fc61d0 ./AUTHORS -13281227f5cd05ddd3c3ee1dac8268fa9740ab89b127acdd6b956e27e26ccd42 ./BUILD.md -c001125f639b4243e9e0a937e8d05676b5f0c41cf05d4a7eaea36f84d6e01a2e ./CHANGELOG.md -29d1c172a94b0d970985cc775160dde12a9bd16793c3db4b4e7bdebfdb6f4534 ./CMakeLists.txt -94093ec2431611fe363c0df1823b81330a8bafae503c68fef86f8bcf89dc3148 ./LICENSE -38d5b9a8d5f1c224b51099f75d4a940d74355ffb64e26ea8395600065b0b34b0 ./README.md -6afb09dcb0101ab4ab9246c78dd83ab2fcb9cac44b16b19f886611ce9439a6fc ./SECURITY.md -e2015d0de6179118c5fce18502ca49f0204510d812cc5f10875d742e0f19c53f ./benchmarks/compiler_bench.cpp -40722e32f25655b225ae0322aa664793922c555b26af9f6bc9d69d93180524ea ./cmake/RunAsmCrossTargets.cmake -f6be29399dc81c62b5bf2cf10e81e56f0223d6f510c925cfd5f6c9195ee03c96 ./cmake/RunAsyncTest.cmake -c3142a276d5985e4be66798fa9ea82a8198fb93b0ee77c40df61e7e65aa978ce ./cmake/RunBareMetalTests.cmake -62bb2a392b13966e0b09041652a2eca7c637a274845b0932a5fcafdafd815fb4 ./cmake/RunCrossTargetTests.cmake -4014016f6249dfa46f9047ada2945951256d39acbec4d0f0a4d9672dee38d5d2 ./cmake/RunExecutableTest.cmake -9b18dbb6ed7cd83ba713bb9cee9ffd3e4cf97764a368b08f91b38d9bb07970ba ./cmake/RunExpectedRuntimeFailure.cmake -339b1e92ba0ec16d9908cd819dd485a7f4e2f1ef42c3ce32ee945542d7384304 ./cmake/RunFailTests.cmake -25e3eb2880bb16f575d1135f9ca72d912047b5186f18509c4d461ab3cc7c5af9 ./cmake/RunHeaderTest.cmake -bd523f12b45228cc39ca3ed8344dc42765e817a3303910a485b72ac230b466a5 ./cmake/RunInvalidAsmConstraint.cmake -833a8372ab485cce78832027c335f7b7ae32674def42ff8b4e0e4e059b7adfc4 ./cmake/RunPackageToolingTest.cmake -fc38e898dff65c9e2e92245d8b5bb645aa83a77496b314b3fd15e86d0922337d ./cmake/RunPassTests.cmake -c411cadae481be36cba2e0562e967063b532595186f10cbb2e7b667b11328d92 ./docs/BOOTSTRAP.md -d0eca505b4d7258025c6f286bbf4d00dabac2706725762d2a721d517c637ac1c ./docs/LANGUAGE_SPEC.md -6362aa3c078a3de2f5105443184924163b1375f1d94655e5074444f7aa032eb8 ./docs/MEMORY_MODEL.md -26bf46d6b272b0f9559ad6c84b5ffd7b72c870c25d37a972cebfb113643a10a0 ./docs/OS_DEVELOPMENT.md -34c585376b439087fa5d5e218800fc87b11d5bebf2b4ec266c35d7fa2d935e52 ./docs/RELEASE_STATUS.md -c4823606c9220dfb0decee9a6ef4b30bdc7f4cb5e747702c74520a50249498ca ./docs/STANDARD_LIBRARY.md -1ea379ed2b4ea7e2f9c89a888d66a18bb86632295fa6518ff4272dbdab9d7ee6 ./docs/TOOLING.md -9eb481d4160e361a1317fd75299a549ef245ecf2e1f337af2511f0976ec03f91 ./docs/UNSAFE_AND_ASM.md -2df90b622b5145b364f1ff1b442022e5685c3444123f5c9d42d46eec2ebc73f1 ./docs/VERIFICATION.md -4f133f80eb71860925cdf4d98fa7a6447f0515912a35b186d37880f948d17bdd ./examples/async_basic.ux -ce328b5ff7c96d32e4313d01707652441f7141c9f55fbba7191a80afac7ab1d1 ./examples/baremetal/aarch64.ld -c6566bd26fcafcf95d3695aa45dbc148fbff26219764eecc4b955af2061b9907 ./examples/baremetal/kernel.ux -6c495ea8b66d733d46819625b3395b232070cc8bb0c9e89f4f1c27ea86dcabba ./examples/baremetal/riscv64.ld -9a37a20b77f4cff51315c3d2f490162954ff3fd7af2dbbe1839808387db23605 ./examples/baremetal/start_aarch64.S -18f8a1e8cb35b608db962ef3cbb5192c94e142d00cad9ba03650ea78c0c25008 ./examples/baremetal/start_riscv64.S -cc16cfbbdce3493d83dd1cbd6e466ddcb9e80988f2c6a0d0392f495640e7edeb ./examples/baremetal/start_x86_64.S -c2922d911968f997703d2aa955587034cf5f94c2d8968f639cbe9a430bb2589b ./examples/baremetal/x86_64.ld -72c7fe0b0e4c1b82b2ee217e2c616ffd804a3827f377c2eb743e81fe6368ba21 ./examples/borrow.ux -54efa8a9737946578435c253bc17c138b5683d540dc798aac8d863a63025267b ./examples/c_ffi.ux -697493b4040c435ea10c034804a0299bc40308c694590fb1fe17fcd348877b62 ./examples/freestanding.ux -4edd651bf708e454cabfc03a0483ac903d7cb58c6f7907cfda1264f61c408196 ./examples/generics.ux -b8e54486afc7ea8f19530dcad3df4d30a34376d8260e1a2619d4aa7af621c0b8 ./examples/hello.ux -3c4f3a20409f73b95c74c0775cf2facd4cce304d20e40c080959d17bdbf1cd9a ./examples/inline_asm.ux -6e5e2961079b16a71062cc77a6b62c6db94e1a28c00a0b484e5f61102d3a8de0 ./fuzz/lexer_fuzz.cpp -db20b0081aecfd0cf717b66dcffee1c12cd27f15e2ca1ccc3301ff8fe51ea5b1 ./fuzz/parser_fuzz.cpp -199c2650120efebf949fc44ae838721fe291552192a2c2985672b990aeb2d089 ./include/uinx/ast.hpp -54e33c5889b689f0c24791e8df485a2539f294325c037c264070752b5b2ec2c3 ./include/uinx/borrow.hpp -e627c095a327f48db9c829d753a00735a7d354df3ea4a3b91bbca84112c3c897 ./include/uinx/codegen.hpp -fad5aad4b510980ca49f2be19dfe60eb42dce686161c193a956d5e704d980594 ./include/uinx/common.hpp -d9f00c1b0708323c32c3c1a649c2fb9c2efc06057e99b92debaa4f51142f6586 ./include/uinx/compiler.hpp -8c4d3cab7ed09fcf877bebbde2ba8c6c0fc7ebe87ce9ebd44867a3c52e84e65a ./include/uinx/diagnostic.hpp -ea9b1e0c06aece0025228d6b80641e479adfde5d7b2bef50e97b97036552958e ./include/uinx/hir.hpp -8973fd2a0fdce5a1b748106fd28e5c4e5ec06de0784a13227f4fac3428c5fdb9 ./include/uinx/lexer.hpp -4773d51b7a570bea12222b9bb671413fcce9da752c4e4c5e9b22876e36d60deb ./include/uinx/mir.hpp -a6bf492aa8d7d5fdee7d3f8aca2953f2c2a565410edb33709754ed49a7b05a0c ./include/uinx/parser.hpp -1281eddc07b531957a4d840c3c7fcfb79bacfb9d36c1f683fbe1e3c2c3d3a8e0 ./include/uinx/sema.hpp -13ba618d731c7a920e70e088b5694d7e3805697b74a49c66fc4978380bcea09e ./include/uinx/token.hpp -49ab9e86ab17c195291e90d2b17aa5432e4015ffbd2259840cef17baebee1cc7 ./include/uinx/types.hpp -18b179d59c9ab967dcf314c22acf958731e0bf8d191bd9cb09a0c0a83f91a24a ./runtime/collections.c -deffb0a24e88eb99176c3bc05477dc8242172b830f6e8b6006b4bad3520733c0 ./runtime/process.c -42b6f8296009190bc4ef94940aa2ded87b531e1c06c4f4772806843f5f2295d0 ./runtime/runtime.c -73e95c8860ec08585882bc32686b25c778cf0a39f822e233bd39606a1106101c ./runtime/runtime.h -15be266c15871d3368949b86c6e48a2e9cf8128a798e819a0f3f73616fd9f385 ./runtime/sync.c -31ea0e81dbd601135d770606196317e9f5b31f04ca2162f41acc0c06df589417 ./runtime/unicode.c -cd9ef28de922a09290d2846c7e20f1363a8be94de46fdd584e5542d89142a1c5 ./src/borrow.cpp -d65ad8c4138056e5ca3c8b57909438753dbab5459cd53390d9b65b0e26e8b117 ./src/codegen.cpp -d97e5e5e099b6fee70a270c8cd60bf12e1434f932db97d9c05732e51587b7181 ./src/compiler.cpp -f06091789c578512d8db781d2262d58a97926b4dabade40dd14b5b5e37e9a9ed ./src/diagnostic.cpp -63f53ce85662e409bbbf3c14b20f31a37a5d5d4a16bf4f83280bed28671f1fd7 ./src/lexer.cpp -fef8b726c374f93f72cfc448925517b85ad5bdf2fb70badcfe7a87f0c8347e61 ./src/mir.cpp -c5a91bc62674fe00f1dd27a55b2a773ad4ad82c2405abd94ec831727d99fe1b9 ./src/parser.cpp -83584eec612cf63ab694adced88df1967c513f42802268ef48fdc64ebad5d086 ./src/sema.cpp -4688334c4f7b60d80e1a9077187f26a2015a99e13cb2ceb78d11d17420ba4b6c ./src/types.cpp -5c8491176b46b0c99024b90f2b080537a0517f846314cc0a242f301104827342 ./stdlib/alloc/arc.ux -e9effdb7ccc40b22fd5b6c560e8d64fb54ca1f6de1710a4940944e6cbb643fe0 ./stdlib/alloc/box.ux -062ca1217dfd111f0e49b491f13097a626c2e1abfb3f6c7656e2e96e5f11d2f2 ./stdlib/alloc/collections.ux -41c56db56718518dd2608692c96503dc1ec0f2e5aa2611ad37e538db6af44219 ./stdlib/alloc/hashmap.ux -2d6a44471aac46e089b05b8805696a5eeb3933b001b6ac95157629d2e0a04839 ./stdlib/alloc/rc.ux -911ea94b12385aa683abaa01bb32493bcc5a0c25dbda4e0d80bc7645ce131dae ./stdlib/alloc/string.ux -04288ef1af81765f8718790b6d7a500df0b582f6bbfc7801c9af65d0edd760c5 ./stdlib/alloc/vec.ux -056673b8e1035e650860a572e7aca3d3ae7595a6e9ad45eafa0ecbd35ae2e485 ./stdlib/core/atomic.ux -248060b9e283745fd71fd88f066f3f68f3a5f90f642861c2131e15149a9746bd ./stdlib/core/iterator.ux -e64f5ba398df592aa3f0860953f5aa190615e3adf08ec02ee8d988167f57070d ./stdlib/core/mem.ux -1054241a43e982f397c6796749e392e21e5763f37018b4232d517ae458939b1e ./stdlib/core/mod.ux -d61336a1568ebc19f4cc7cdb24f54270abede8499f4d22be57d98af808bdbe38 ./stdlib/core/option.ux -ae2cb907e662feb58cf275f3deb90b0eab61fcca7064d4be61b7561ab20ca457 ./stdlib/core/ptr.ux -b7184859c6e8a3bd7c35a1d1de4f85d38f52822741b9a065e11076fc79f569ab ./stdlib/core/result.ux -4f1eee52254b86c556fd26450b2c2cb99b55f3ccbc627e73df838172bacbc488 ./stdlib/core/slice.ux -38316b801ddc3cc1bf6781e5b648e93120334117af54800b8c85639d03491e8c ./stdlib/core/sync.ux -d8f47ef438a7662b3e1953d3cf5af52e567570b72c4844690008c3b7065e51f5 ./stdlib/core/traits.ux -3b5d7df8ffefcdf0539fba118ea04860254fbbb09db3e577382cf4c1b7a25419 ./stdlib/minimal/fs.ux -f3d80045ab31a7346e0d9bd62a51e58011d807521d5325f032a7c1c0d27e250e ./stdlib/minimal/io.ux -b09bd798a4dd3d690ba67c2d5eff4e83ef389adbaf3d0d3f5c78be68763e1dc3 ./stdlib/minimal/net.ux -f2363ba09deefd24d017798c9c1dde546a7134727dc589d3863e782394706d37 ./stdlib/minimal/sync.ux -b60dc248e41a504a32511aae7ba2f18255a13ff7495494c12b1fcb6d391b35ec ./stdlib/minimal/thread.ux -216066a3c901f537e41670d049598b46726fd81865ff219dc0172f2011e05733 ./stdlib/minimal/time.ux -65af1dcc222cf67a0f1a4e205bfd221bc18925c71ade5550d1e842dc6d3f0638 ./stdlib/std/async.ux -a6e4792fbe0f84529edad1d8743aa9eb9025d7c6da39c04ee8ee7a7bc1998640 ./stdlib/std/collections.ux -f4914105d5021a21ee86ba192109dca8177770c2818e396a0be3f9e0d3bbf616 ./stdlib/std/fs.ux -4dd4de3946fdba41a7c80767c7e623a27f0da6438bda0125c975c59ca8b77d12 ./stdlib/std/ipc.ux -a0d40ccabff2d0b5cb563be54015652dcd23b44ad53f60ce79c2f3776bd0a9e8 ./stdlib/std/mod.ux -21857a9ab8e5e1cdda958a021c7e034216c3af75de732dc8ba059044608c593c ./stdlib/std/net.ux -c8291a24712b458a1d86fd3c8070604800e1c6498f35cf0d58d346ae384d7a39 ./stdlib/std/process.ux -58b62f4c447c42abc185605aaebf4f59c87a1ec99bf31faa9d1dca651fbc6564 ./stdlib/std/thread.ux -514a1a1394397397d4994084f6c897d0fd27900f3f8a454bfa7c05f8688cb5b7 ./stdlib/std/unicode.ux -10ad10ac176deaf241629efa500fd10977adeaf71c8b5f2a0d69ef6d69666c73 ./tests/README.md -a2299b4ca9d8d5e9ee6ec884d9292e2e6acafe7dc16f2d1448ab14e30e7ebbdd ./tests/codegen/asm/cross_nop.ux -249009dc5cca8a887a30eb0292beb4fb08e6f16770d0a04c5be4ddd6c3596f8f ./tests/codegen/asm/invalid_aarch64_reg.ux -6140379949bae960a9a0d56f98e0c6a726f95ec345cc9e295ba36a482a3145d8 ./tests/codegen/asm/x86_64_inout.ux -61ccb4cb7aa7e8a81d7f745d1b5ea05d0013bd8882df7f10e6a9a311cfadd33e ./tests/codegen/asm/x86_64_io.ux -c1fd98b3995e18a7cbfbadefdb6b1fc8f0d69b5082167db674ff77440090dfb2 ./tests/codegen/asm/x86_64_multiout.ux -ba6e1b9db5df419ba0e4d674bed8f27c7c23e3d4c1124966a06d2f0ceabe1368 ./tests/codegen/asm/x86_64_nop.ux -d6c8d905afbac42188f61bff7eb8213036ff1be0b9f18bcc1d83db0c260b395b ./tests/conformance/fail/header_requires_enable.ux -807e368b72084d1986d7e96d3890d635fb9564ea6c27af08e7e6591528b894ea ./tests/conformance/fail/immutable_assign.ux -ca7f0ebec3af5e3b752d46c761e8a8885a4557a615fd44b4f129cca91f630626 ./tests/conformance/fail/immutable_mut_borrow.ux -8ab274516551a2bccd312e12f9d3d0de629c667be9ce068301ff007e7fb53832 ./tests/conformance/fail/trait_missing_method.ux -17b714490b9cff520e0dbc832f255dea44c61bd49ce34f3bbd6c64da3beb394b ./tests/conformance/fail/type_mismatch.ux -888019a3654df9c0f7501c15f29fb1cccba72d360da2bdc833ff63c8fffffc9e ./tests/conformance/fail/unsafe_required.ux -6fa5244961330664f31d5684b277a843678a17706ce7d50fc30de15a05a3f9e3 ./tests/conformance/pass/control_flow_extended.ux -bc1ca3f298ba7abf3eaf77eb36e83567aec6c7db0c9700a1f27b69c64b710dac ./tests/conformance/pass/field_borrow.ux -b8af37f455cd7ab8f0f400c422ff290730f99d18612a0d96c2bb3fbfb8ce4ffb ./tests/conformance/pass/generic.ux -e87a450e492ca0cc121ec442c464342b269acfe6ef8684e8022fdd78eb3c3d15 ./tests/conformance/pass/globals_and_bitops.ux -d11a24b1833b38ce16796ba890d0132ae9ed77816e50044f5e973a6b164c320a ./tests/conformance/pass/hello.ux -05cfc3a78c198165f1ef7b813aaaa6df43da7f10a479ef2f4ae3b50c22a78cf4 ./tests/conformance/pass/methods.ux -9c4b8158b295e4e4a1e22600eba64cadec36e9d35a1161f49529c81cdb4f471a ./tests/conformance/pass/mutation.ux -fcf70321a7f0d82d02b35f86c4154c1a6b51321535a75987c508b7eb045c7f5a ./tests/conformance/pass/need_dontneed.ux -a0cc902c80d03f810a20eb1417dc9e78070137e482fd230a7726167f34549993 ./tests/conformance/pass/os_smp_syntax.ux -104bf4000fe1c11ea1b64ba123a44e1f865eb37514f6d6ab2aecbbfcf07474d6 ./tests/conformance/pass/raii_drop.ux -ca7e5abfc7a33de046f06a9a208792852efc33118f73fe40c8221b189d03b49d ./tests/conformance/pass/reference_parameter_return.ux -6d280f81c5345b28d8e38e4c86f881725c287808e56df6da06fa484a01853386 ./tests/conformance/pass/send_marker.ux -9139044de246ef16607cbd4a04c661d85a54b45c858e2d666d2683a54bc9cb30 ./tests/conformance/pass/slice_index.ux -b30096d33a39242f2e214b709e841592742ed7601ec3adef8980a751693299ce ./tests/conformance/pass/slice_oob.ux -74486259d9b4a1ec1ad476006dc45151381037d978f24ffeac61da1645080c0b ./tests/conformance/pass/trait_method.ux -4f02c40dc3aa53adfcfd5093fe259349f7bc0d490396c9e547f927789d31e586 ./tests/headers/include/a.uxh -9fa8bc15455ce4e8ab3cfc49ae3ca9ce04826814b5063c4e027371359249079d ./tests/headers/include/b.uxh -ce7a6f4f715702a34a348334b43ce5151f194fd01658a37935ab94a76e1f2b55 ./tests/headers/include/common.uxh -a2022fff359299748d00239fc3261c352e38f5521955f4b8a7c3149cfb6e0e38 ./tests/headers/include/decl_a.uxh -a2022fff359299748d00239fc3261c352e38f5521955f4b8a7c3149cfb6e0e38 ./tests/headers/include/decl_b.uxh -377d3305ee9b54e8c5c94dc7caa30912f1860c03f9b8f0a1809c70daf477ada3 ./tests/headers/include/math.uxh -130d2a5b0398b290a6502d6a00dbf481bb8e4c164dfae433ac0d5bb799770d9d ./tests/headers/main.ux -d68386e117f417ac0e274af42ca638392b6e36fe1b08053011d621262c35eadf ./tests/runtime/async/suspend_resume.ux -1ad04285e9d4de6ff5d7190eb5379af262cc95747ab33e1e2b45bc089dde6182 ./tests/safety/borrow-fail/alias_mut.ux -eba504578023566623e5ad6268f13160e05309956486e203a25e9f705420378b ./tests/safety/borrow-fail/dangling_block_assignment.ux -401d0fd93f75dc0d090fe679b409c8760f9bf5f66ac27d4d29568dbdbc3f9b08 ./tests/safety/borrow-fail/dangling_return.ux -f260991bcb0cea16f94e5d95fa75de0a9b6826912e1eeb7907eda1966f1c4eec ./tests/safety/borrow-fail/dangling_struct_field.ux -b552d8caa51515ce7b4f136399e046943de8fdff2180cd948d768cbdc4bc17cb ./tests/safety/borrow-fail/dangling_through_call.ux -c53e0e5c1c76bc3609ab421b3013dbcbbe891370dedd86d211afb7fc8e40412a ./tests/safety/borrow-fail/index_alias.ux -407cb2deee1ebad7931bdbb5a2aa8ac5c745204efdfbb62fd1f622e6a8c18f39 ./tests/safety/borrow-fail/mutable_ref_move.ux -ab910b2549d53744584a8ea2cab2a268c8f56e840d03c6223705b21e089ff745 ./tests/safety/borrow-fail/use_after_move.ux -d1f295a3a93f144937ea0be61d8c9fcfea77d93ec78c7904ae5144d2c8566855 ./tests/safety/smp-fail/concurrent_missing_send.ux -4e6b7fb362831020ce1cf1b7860c8e7e53ea9be4603054dc0fc201dd42203cbd ./tests/safety/smp-fail/concurrent_missing_sync.ux -d3a691a311498864337c3144b118cc6b0c90f6b4cc341ad6eb2eb07c07ddc90e ./tests/safety/smp-fail/concurrent_raw_pointer.ux -0a90783c60d6681398fda52af892b29d79c11386f0f90d23de186bf0d4a70c0c ./tests/support/async_harness.c -9cfbbcac5f3e801a1a9e4db5fe434c437f027b3aa5d6441be03e83fde7059777 ./tests/unit/compiler_unit.cpp -e5cc65b18e618d9e3a5cead04be04a5bd552f15f5f09fe004e53bf2a210285a0 ./tests/unit/runtime_unit.c -bc600c56e2b25f8eb4a677550fcc11a4e2b22abd57c2bd15ee369b8d45393d8c ./tools/uinx.cpp -9299473fd83f32c86dfb0b85d6c3bf19174e2edb48e6004431da865a530c25d3 ./tools/uinx_lsp.cpp -19e1cf02f2378839992a85136644db2c6e8f0301ea4bc7ee07c83610b6b5c088 ./tools/uinxc.cpp -a2d31fd350b48b78bf9f83ade49f57d8e150dce23864ec66f415d2d54ce886ae ./verification/README.md diff --git a/README.md b/README.md index 26e6098..ce3ca87 100644 --- a/README.md +++ b/README.md @@ -380,6 +380,9 @@ boundary rather than hidden behind a wrapper that simply invokes the C++ compile | Document | Purpose | |---|---| +| `docs/BUILD.md` | build, install and cross-target instructions | +| `docs/ARCHITECTURE.md` | compiler pipeline and runtime design | +| `docs/TOOLING.md` | `uinx` / `uinxc` / `uinx-lsp` usage | | `docs/LANGUAGE_SPEC.md` | canonical implemented grammar and semantics | | `docs/MEMORY_MODEL.md` | ownership, aliasing, atomics, SMP, fences | | `docs/BOOTSTRAP.md` | honest self-hosting/bootstrap acceptance criteria | diff --git a/ARCHITECTURE.md b/docs/ARCHITECTURE.md similarity index 100% rename from ARCHITECTURE.md rename to docs/ARCHITECTURE.md diff --git a/BUILD.md b/docs/BUILD.md similarity index 86% rename from BUILD.md rename to docs/BUILD.md index ef2393a..e9718e3 100644 --- a/BUILD.md +++ b/docs/BUILD.md @@ -15,6 +15,20 @@ Verified release environment: The project intentionally does not require LLVM C++ development headers. `uinxc` emits LLVM IR directly and invokes a configured Clang driver for LLVM IR to object conversion. Object linking is a separate phase. +## Presets + +`CMakePresets.json` provides `debug`, `release`, `ci` (Release with warnings as +errors), `asan` (ASan + UBSan on the C++ compiler code) and `fuzz`: + +```sh +cmake --preset ci +cmake --build --preset ci +ctest --preset ci +``` + +Build trees go to `build//`. Local overrides belong in the git-ignored +`CMakeUserPresets.json`. + ## Release build ```sh diff --git a/include/uinx/mir.hpp b/include/uinx/mir.hpp index 70976db..d721096 100644 --- a/include/uinx/mir.hpp +++ b/include/uinx/mir.hpp @@ -144,11 +144,8 @@ class MIRLowerer { std::string lower_place_address(const ast::Expr& e, FnState& s); Type expr_type(const ast::Expr& e, const FnState& s) const; Type subst_type(const Type& t, const FnState& s) const; - std::string coerce_value(FnState& s, - std::string value, - const Type& from, - const Type& to, - const SourceRange& range); + std::string coerce_value( + FnState& s, std::string value, const Type& from, const Type& to, const SourceRange& range); std::string specialize_call(const ast::CallExpr& c, const FunctionSig& sig, FnState& s); void emit_scope_drops(FnState& s); bool place_is_atomic(const ast::Expr& e, const FnState& s) const; diff --git a/include/uinx/types.hpp b/include/uinx/types.hpp index 9b5a08d..f75213f 100644 --- a/include/uinx/types.hpp +++ b/include/uinx/types.hpp @@ -6,6 +6,7 @@ #include "uinx/ast.hpp" #include +#include namespace uinx { enum class TypeKind { @@ -41,6 +42,16 @@ struct Type { std::shared_ptr pointee; bool mut{false}; bool nullable{false}; + Type() = default; + Type(const Type&) = default; + Type(Type&&) noexcept = default; + Type& operator=(Type&&) noexcept = default; + // `t = *t.pointee` must not destroy the source before it is copied, so build + // the copy first and then move it into place. + Type& operator=(const Type& other) { + Type copy{other}; + return *this = std::move(copy); + } static Type builtin(std::string_view name); static Type named(std::string name, std::vector args = {}); static Type generic(std::string name); diff --git a/src/codegen.cpp b/src/codegen.cpp index 2f44cca..99a5544 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -646,7 +646,7 @@ bool LLVMCodegen::emit_syscall(std::ostream& os, std::unordered_map& values, std::unordered_map& value_types, std::size_t& counter) const { - const int arity = in.args.size(); + const int arity = static_cast(in.args.size()); if (arity < 1 || arity > 7) return false; diff --git a/src/mir.cpp b/src/mir.cpp index 6b5a496..233f0ee 100644 --- a/src/mir.cpp +++ b/src/mir.cpp @@ -765,9 +765,7 @@ std::pair MIRLowerer::lower_expr(const ast::Expr& e, FnState& std::unordered_set field_generics(si->second.generic_names.begin(), si->second.generic_names.end()); std::unordered_map field_subst; - for (std::size_t i = 0; - i < si->second.generic_names.size() && i < ty.args.size(); - ++i) + for (std::size_t i = 0; i < si->second.generic_names.size() && i < ty.args.size(); ++i) field_subst[si->second.generic_names[i]] = ty.args[i]; std::vector args; for (const auto& decl_field : si->second.decl->fields) { @@ -777,8 +775,8 @@ std::pair MIRLowerer::lower_expr(const ast::Expr& e, FnState& continue; } auto [field_value, field_actual] = lower_expr(*it->second, s); - const Type field_expected = substitute_type( - type_from_ast(decl_field.type, field_generics), field_subst); + const Type field_expected = + substitute_type(type_from_ast(decl_field.type, field_generics), field_subst); args.push_back( coerce_value(s, field_value, field_actual, field_expected, it->second->range)); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..5b04103 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +add_executable(uinx-unit-tests unit/compiler_unit.cpp) +target_link_libraries(uinx-unit-tests PRIVATE uinx_compiler) +add_test(NAME unit.compiler COMMAND uinx-unit-tests) +add_executable(uinx-runtime-tests unit/runtime_unit.c) +target_link_libraries(uinx-runtime-tests PRIVATE uinx_runtime) +add_test(NAME unit.runtime COMMAND uinx-runtime-tests) +add_test(NAME codegen.asm-object COMMAND $ ${PROJECT_SOURCE_DIR}/tests/codegen/asm/x86_64_nop.ux --emit=obj -o ${PROJECT_BINARY_DIR}/asm-test.o) +add_test(NAME codegen.asm-input-output COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DSOURCE=${PROJECT_SOURCE_DIR}/tests/codegen/asm/x86_64_io.ux -DOUTPUT=${PROJECT_BINARY_DIR}/asm-io -P ${PROJECT_SOURCE_DIR}/cmake/RunExecutableTest.cmake) +add_test(NAME codegen.asm-inout COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DSOURCE=${PROJECT_SOURCE_DIR}/tests/codegen/asm/x86_64_inout.ux -DOUTPUT=${PROJECT_BINARY_DIR}/asm-inout -P ${PROJECT_SOURCE_DIR}/cmake/RunExecutableTest.cmake) +add_test(NAME codegen.asm-multiple-outputs COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DSOURCE=${PROJECT_SOURCE_DIR}/tests/codegen/asm/x86_64_multiout.ux -DOUTPUT=${PROJECT_BINARY_DIR}/asm-multiout -P ${PROJECT_SOURCE_DIR}/cmake/RunExecutableTest.cmake) +add_test(NAME runtime.slice-index-bounds COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DSOURCE=${PROJECT_SOURCE_DIR}/tests/conformance/pass/slice_index.ux -DOUTPUT=${PROJECT_BINARY_DIR}/slice-index -P ${PROJECT_SOURCE_DIR}/cmake/RunExecutableTest.cmake) +add_test(NAME runtime.slice-index-oob-traps COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DSOURCE=${PROJECT_SOURCE_DIR}/tests/conformance/pass/slice_oob.ux -DOUTPUT=${PROJECT_BINARY_DIR}/slice-oob -P ${PROJECT_SOURCE_DIR}/cmake/RunExpectedRuntimeFailure.cmake) +add_test(NAME runtime.c-ffi COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DSOURCE=${PROJECT_SOURCE_DIR}/examples/c_ffi.ux -DOUTPUT=${PROJECT_BINARY_DIR}/c-ffi -P ${PROJECT_SOURCE_DIR}/cmake/RunExecutableTest.cmake) +add_test(NAME headers.enabled COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DSOURCE=${PROJECT_SOURCE_DIR}/tests/headers/main.ux -DINCLUDE_DIR=${PROJECT_SOURCE_DIR}/tests/headers/include -DOUTPUT=${PROJECT_BINARY_DIR}/header-test -P ${PROJECT_SOURCE_DIR}/cmake/RunHeaderTest.cmake) +add_test(NAME codegen.cross-target-objects COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DSOURCE=${PROJECT_SOURCE_DIR}/examples/freestanding.ux -DBINARY_DIR=${PROJECT_BINARY_DIR} -P ${PROJECT_SOURCE_DIR}/cmake/RunCrossTargetTests.cmake) +add_test(NAME codegen.asm-cross-targets COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DSOURCE=${PROJECT_SOURCE_DIR}/tests/codegen/asm/cross_nop.ux -DBINARY_DIR=${PROJECT_BINARY_DIR} -P ${PROJECT_SOURCE_DIR}/cmake/RunAsmCrossTargets.cmake) +add_test(NAME codegen.asm-invalid-target-register COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DSOURCE=${PROJECT_SOURCE_DIR}/tests/codegen/asm/invalid_aarch64_reg.ux -DOUTPUT=${PROJECT_BINARY_DIR}/asm-invalid.o -P ${PROJECT_SOURCE_DIR}/cmake/RunInvalidAsmConstraint.cmake) +add_test(NAME codegen.baremetal-cross-link COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DCLANG=clang -DLD_LLD=ld.lld -DSOURCE_DIR=${PROJECT_SOURCE_DIR}/examples/baremetal -DBINARY_DIR=${PROJECT_BINARY_DIR} -P ${PROJECT_SOURCE_DIR}/cmake/RunBareMetalTests.cmake) +add_test(NAME runtime.async-suspend-resume COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DSOURCE=${PROJECT_SOURCE_DIR}/tests/runtime/async/suspend_resume.ux -DUINX_OBJ=${PROJECT_BINARY_DIR}/async-test.o -DCLANG=clang -DRUNTIME_INCLUDE=${PROJECT_SOURCE_DIR}/runtime -DHARNESS=${PROJECT_SOURCE_DIR}/tests/support/async_harness.c -DRUNTIME_LIB=$ -DOUTPUT=${PROJECT_BINARY_DIR}/async-test -P ${PROJECT_SOURCE_DIR}/cmake/RunAsyncTest.cmake) +add_test(NAME conformance.positive COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DTEST_DIR=${PROJECT_SOURCE_DIR}/tests/conformance/pass -P ${PROJECT_SOURCE_DIR}/cmake/RunPassTests.cmake) +add_test(NAME conformance.negative COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DTEST_DIR=${PROJECT_SOURCE_DIR}/tests/conformance/fail -P ${PROJECT_SOURCE_DIR}/cmake/RunFailTests.cmake) +add_test(NAME safety.borrow-negative COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DTEST_DIR=${PROJECT_SOURCE_DIR}/tests/safety/borrow-fail -P ${PROJECT_SOURCE_DIR}/cmake/RunFailTests.cmake) +add_test(NAME safety.smp-negative COMMAND ${CMAKE_COMMAND} -DUINXC=$ -DTEST_DIR=${PROJECT_SOURCE_DIR}/tests/safety/smp-fail -P ${PROJECT_SOURCE_DIR}/cmake/RunFailTests.cmake) +add_test(NAME tooling.package-integration COMMAND ${CMAKE_COMMAND} -DUINX=$ -DWORK=${PROJECT_BINARY_DIR}/package-tooling -P ${PROJECT_SOURCE_DIR}/cmake/RunPackageToolingTest.cmake) diff --git a/tools/uinx.cpp b/tools/uinx.cpp index e694c93..7d245ca 100644 --- a/tools/uinx.cpp +++ b/tools/uinx.cpp @@ -106,9 +106,8 @@ SourceSelection source_selection(const std::filesystem::path& root, const Manife if (name == "std") selection.std_mode = disabled ? "none" : "full"; - else if (!disabled && - (name == "core" || name == "alloc" || name == "sys" || name == "os" || - name == "fmt" || name == "minimal")) + else if (!disabled && (name == "core" || name == "alloc" || name == "sys" || name == "os" || + name == "fmt" || name == "minimal")) selection.std_mode = name; else if (name == "runtime") selection.runtime = !disabled; @@ -794,14 +793,8 @@ int main(int argc, char** argv) { return 2; } std::filesystem::path executable; - const int result = - build_project(root, - manifest, - EmitKind::Executable, - release, - &executable, - smp_override, - auto_repair); + const int result = build_project( + root, manifest, EmitKind::Executable, release, &executable, smp_override, auto_repair); if (result) return result; return std::system(("\"" + executable.string() + "\"").c_str()); diff --git a/verification/README.md b/verification/README.md deleted file mode 100644 index a52087e..0000000 --- a/verification/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Verification Workspace Policy - -This directory intentionally contains no checked-in test transcripts. - -Use `docs/VERIFICATION.md` for the reproducible commands. Local/CI build logs, temporary probe programs, benchmark output, and CTest transcripts are disposable artifacts and should be removed after verification rather than accumulated in the source tree. diff --git a/verification/compiler-benchmark.txt b/verification/compiler-benchmark.txt deleted file mode 100644 index 23a16ce..0000000 --- a/verification/compiler-benchmark.txt +++ /dev/null @@ -1,3 +0,0 @@ -lexer_ns_per_iter=5545 -parse_ns_per_iter=8628 -check_pipeline_ns_per_iter=29009 diff --git a/verification/ctest-release.txt b/verification/ctest-release.txt deleted file mode 100644 index ee978c9..0000000 --- a/verification/ctest-release.txt +++ /dev/null @@ -1,42 +0,0 @@ -Internal ctest changing into directory: Uinx-Language-0.3.0/build-final -Test project Uinx-Language-0.3.0/build-final - Start 1: unit.compiler - 1/18 Test #1: unit.compiler ......................... Passed 0.74 sec - Start 2: unit.runtime - 2/18 Test #2: unit.runtime .......................... Passed 0.02 sec - Start 3: codegen.asm-object - 3/18 Test #3: codegen.asm-object .................... Passed 0.24 sec - Start 4: codegen.asm-input-output - 4/18 Test #4: codegen.asm-input-output .............. Passed 0.37 sec - Start 5: codegen.asm-inout - 5/18 Test #5: codegen.asm-inout ..................... Passed 0.38 sec - Start 6: codegen.asm-multiple-outputs - 6/18 Test #6: codegen.asm-multiple-outputs .......... Passed 0.38 sec - Start 7: runtime.slice-index-bounds - 7/18 Test #7: runtime.slice-index-bounds ............ Passed 0.40 sec - Start 8: runtime.slice-index-oob-traps - 8/18 Test #8: runtime.slice-index-oob-traps ......... Passed 0.38 sec - Start 9: runtime.c-ffi - 9/18 Test #9: runtime.c-ffi ......................... Passed 0.38 sec - Start 10: codegen.cross-target-objects -10/18 Test #10: codegen.cross-target-objects .......... Passed 0.32 sec - Start 11: codegen.asm-cross-targets -11/18 Test #11: codegen.asm-cross-targets ............. Passed 0.23 sec - Start 12: codegen.asm-invalid-target-register -12/18 Test #12: codegen.asm-invalid-target-register ... Passed 0.06 sec - Start 13: codegen.baremetal-cross-link -13/18 Test #13: codegen.baremetal-cross-link .......... Passed 0.98 sec - Start 14: runtime.async-suspend-resume -14/18 Test #14: runtime.async-suspend-resume .......... Passed 0.48 sec - Start 15: conformance.positive -15/18 Test #15: conformance.positive .................. Passed 2.96 sec - Start 16: conformance.negative -16/18 Test #16: conformance.negative .................. Passed 0.14 sec - Start 17: safety.borrow-negative -17/18 Test #17: safety.borrow-negative ................ Passed 0.17 sec - Start 18: tooling.package-integration -18/18 Test #18: tooling.package-integration ........... Passed 2.45 sec - -100% tests passed, 0 tests failed out of 18 - -Total Test time (real) = 11.09 sec diff --git a/verification/environment.txt b/verification/environment.txt deleted file mode 100644 index f222c35..0000000 --- a/verification/environment.txt +++ /dev/null @@ -1,11 +0,0 @@ -Uinx 0.3 verification environment -UTC: 2026-08-19T05:39:56Z -Linux a40a20264636 6.18.35 #1 SMP Mon Jul 27 18:07:50 UTC 2026 x86_64 GNU/Linux - -cmake version 3.31.6 -c++ (Debian 14.2.0-19) 14.2.0 -cc (Debian 14.2.0-19) 14.2.0 -clang version 17.0.0 (https://github.com/swiftlang/llvm-project.git 10999b6d034fe318f3d56c83bddb6572593a8bb0) -LLD 17.0.0 (https://github.com/swiftlang/llvm-project.git 10999b6d034fe318f3d56c83bddb6572593a8bb0) (compatible with GNU linkers) -ninja 1.12.1 -clang-format: not installed in verification container diff --git a/verification/examples-check.txt b/verification/examples-check.txt deleted file mode 100644 index e787c94..0000000 --- a/verification/examples-check.txt +++ /dev/null @@ -1,25 +0,0 @@ -$ build-final/uinxc examples/async_basic.ux --emit=check -PASS - -$ build-final/uinxc examples/baremetal/kernel.ux --emit=check -PASS - -$ build-final/uinxc examples/borrow.ux --emit=check -PASS - -$ build-final/uinxc examples/c_ffi.ux --emit=check -PASS - -$ build-final/uinxc examples/freestanding.ux --emit=check -PASS - -$ build-final/uinxc examples/generics.ux --emit=check -PASS - -$ build-final/uinxc examples/hello.ux --emit=check -PASS - -$ build-final/uinxc examples/inline_asm.ux --emit=check -PASS - -Checked 8 example programs: PASS diff --git a/verification/optimizer-check.txt b/verification/optimizer-check.txt deleted file mode 100644 index b0eaa66..0000000 --- a/verification/optimizer-check.txt +++ /dev/null @@ -1,6 +0,0 @@ -MIR optimizer probe -O0 IR lines: 29 -O2 IR lines: 22 -O0 i32 loads: 6 -O2 i32 loads: 0 -O2 folded expression present: yes diff --git a/verification/os-freestanding.txt b/verification/os-freestanding.txt deleted file mode 100644 index 95eedb4..0000000 --- a/verification/os-freestanding.txt +++ /dev/null @@ -1,11 +0,0 @@ -Freestanding kernel/core cross-target verification -x86_64 package kernel: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped -aarch64 package kernel: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, not stripped -riscv64 package kernel: ELF 64-bit LSB executable, UCB RISC-V, RVC, soft-float ABI, version 1 (SYSV), statically linked, not stripped - -x86_64 direct example: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped -aarch64 direct example: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, not stripped -riscv64 direct example: ELF 64-bit LSB executable, UCB RISC-V, RVC, soft-float ABI, version 1 (SYSV), statically linked, not stripped - -core::atomic + core::ptr + core::sync use test: x86_64/AArch64/RISC-V64 objects had no undefined symbols. -core::mem pure-Uinx object generation: verified for x86_64/AArch64/RISC-V64. diff --git a/verification/release-build.txt b/verification/release-build.txt deleted file mode 100644 index 05dfd76..0000000 --- a/verification/release-build.txt +++ /dev/null @@ -1,6 +0,0 @@ -$ cmake -S . -B build-final -G Ninja -DCMAKE_BUILD_TYPE=Release -Result: PASS -$ cmake --build build-final -j2 -Result: PASS (28 build steps; 0 compiler warnings) -$ build-final/uinx --version -uinx 0.3.0 diff --git a/verification/stdlib-check.txt b/verification/stdlib-check.txt deleted file mode 100644 index 9f83e67..0000000 --- a/verification/stdlib-check.txt +++ /dev/null @@ -1,2 +0,0 @@ -Command: build-final/uinxc --emit=check -Result: PASS From 8114a8f9eacfb297dfdc974f79ba5db390f76357 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 12:00:50 +0000 Subject: [PATCH 2/4] refactor: split borrow, sema and codegen into focused translation units Pure move, no behavior change. borrow.cpp (1894 lines), sema.cpp (1795) and codegen.cpp (1881) are split by responsibility; helpers formerly in anonymous namespaces move to src/*_internal.hpp. LLVM IR and diagnostics for all 56 examples/tests are byte-identical before and after. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_018uUuRXQcBduAQvVUcJYkWz --- CMakeLists.txt | 12 + src/borrow.cpp | 1486 +---------------------------------- src/borrow_flow.cpp | 266 +++++++ src/borrow_internal.hpp | 58 ++ src/borrow_liveness.cpp | 472 +++++++++++ src/borrow_loans.cpp | 326 ++++++++ src/borrow_places.cpp | 426 ++++++++++ src/codegen.cpp | 1616 +------------------------------------- src/codegen_asm.cpp | 303 +++++++ src/codegen_async.cpp | 556 +++++++++++++ src/codegen_function.cpp | 457 +++++++++++ src/codegen_internal.hpp | 361 +++++++++ src/sema.cpp | 1481 +--------------------------------- src/sema_expr.cpp | 494 ++++++++++++ src/sema_internal.hpp | 72 ++ src/sema_resolve.cpp | 384 +++++++++ src/sema_smp.cpp | 372 +++++++++ src/sema_stmt.cpp | 230 ++++++ 18 files changed, 4798 insertions(+), 4574 deletions(-) create mode 100644 src/borrow_flow.cpp create mode 100644 src/borrow_internal.hpp create mode 100644 src/borrow_liveness.cpp create mode 100644 src/borrow_loans.cpp create mode 100644 src/borrow_places.cpp create mode 100644 src/codegen_asm.cpp create mode 100644 src/codegen_async.cpp create mode 100644 src/codegen_function.cpp create mode 100644 src/codegen_internal.hpp create mode 100644 src/sema_expr.cpp create mode 100644 src/sema_internal.hpp create mode 100644 src/sema_resolve.cpp create mode 100644 src/sema_smp.cpp create mode 100644 src/sema_stmt.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 94dc470..60a61c2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,9 +40,20 @@ add_library(uinx_compiler src/parser.cpp src/types.cpp src/sema.cpp + src/sema_resolve.cpp + src/sema_smp.cpp + src/sema_stmt.cpp + src/sema_expr.cpp src/borrow.cpp + src/borrow_places.cpp + src/borrow_loans.cpp + src/borrow_liveness.cpp + src/borrow_flow.cpp src/mir.cpp src/codegen.cpp + src/codegen_asm.cpp + src/codegen_async.cpp + src/codegen_function.cpp src/compiler.cpp ) target_include_directories(uinx_compiler PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) @@ -74,6 +85,7 @@ if(CLANG_FORMAT_EXECUTABLE) file(GLOB_RECURSE UINX_FORMAT_SOURCES CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/include/*.hpp ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/*.hpp ${CMAKE_CURRENT_SOURCE_DIR}/tools/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/runtime/*.c ${CMAKE_CURRENT_SOURCE_DIR}/runtime/*.h diff --git a/src/borrow.cpp b/src/borrow.cpp index feb3cbe..fe3e4c6 100644 --- a/src/borrow.cpp +++ b/src/borrow.cpp @@ -4,1493 +4,13 @@ #include "uinx/borrow.hpp" +#include "borrow_internal.hpp" + #include #include namespace uinx { -namespace { -constexpr std::size_t kMaxDataflowIterations = 4096; - -std::string root_of(std::string_view place) { - const auto dot = place.find('.'); - return std::string(place.substr(0, dot)); -} - -bool is_strict_child(std::string_view parent, std::string_view child) { - return child.size() > parent.size() && child.substr(0, parent.size()) == parent && - child[parent.size()] == '.'; -} - -std::string symbol_key(std::string_view name, hir::SymbolId id) { - return std::string(name) + "#" + std::to_string(id); -} - -std::string display_place(std::string_view place) { - if (place.starts_with(" split_place(std::string_view place) { - std::vector parts; - std::size_t start = 0; - while (true) { - const auto dot = place.find('.', start); - if (dot == std::string_view::npos) { - parts.push_back(std::string(place.substr(start))); - break; - } - parts.push_back(std::string(place.substr(start, dot - start))); - start = dot + 1; - } - return parts; -} - -bool BorrowChecker::places_disjoint(std::string_view a, std::string_view b) const { - // Conservative external provenance always conflicts. - if (a.starts_with(" parent/child overlap. - return false; -} - -bool BorrowChecker::places_conflict(std::string_view a, std::string_view b) const { - return !places_disjoint(a, b); -} - -bool BorrowChecker::loan_live_at(const Borrow& loan, - const LiveSet& live_roots, - const LivePlaceSet& live_places) const { - if (loan.borrower == "") - return true; - const std::string borrower_root = root_of(loan.borrower); - if (model_.globals.contains(borrower_root)) - return true; - // Polonius-style: a loan is live only if its borrower may be used later. - // Check both root liveness (NLL) and precise place liveness. If either - // indicates a future use, the loan stays live. - if (live_roots.contains(borrower_root)) - return true; - if (live_places.contains(loan.borrower)) - return true; - // Subset edges: if any live place overlaps the borrower (e.g. a field of - // the borrower is live), the loan stays live. - for (const auto& live : live_places) { - if (places_conflict(live, loan.borrower)) - return true; - } - return false; -} - -bool BorrowChecker::same_origin(const RefOrigin& a, const RefOrigin& b) const { - return a.place == b.place && a.mut == b.mut && a.range.begin.line == b.range.begin.line && - a.range.begin.column == b.range.begin.column; -} - -bool BorrowChecker::same_borrow(const Borrow& a, const Borrow& b) const { - return a.place == b.place && a.borrower == b.borrower && a.mut == b.mut && a.kind == b.kind && - a.origin.begin.line == b.origin.begin.line && - a.origin.begin.column == b.origin.begin.column; -} - -std::optional BorrowChecker::place_of(const ast::Expr& expr) const { - if (expr.kind == ast::ExprKind::Name) { - const auto& name = static_cast(expr).name; - const auto resolved = model_.hir.expr_resolution.find(&expr); - if (resolved != model_.hir.expr_resolution.end() && - resolved->second < model_.hir.symbols.size()) { - const auto& symbol = model_.hir.symbols[resolved->second]; - if (symbol.kind == hir::SymbolKind::Local || symbol.kind == hir::SymbolKind::Parameter) - return symbol_key(name, resolved->second); - } - return name; - } - if (expr.kind == ast::ExprKind::Member) { - const auto& member = static_cast(expr); - auto base = place_of(*member.base); - if (base) - return *base + "." + member.member; - } - if (expr.kind == ast::ExprKind::Index) { - const auto& index = static_cast(expr); - auto base = place_of(*index.base); - if (base) - return *base + ".[*]"; - } - return std::nullopt; -} - -std::string BorrowChecker::binding_key(const ast::LetStmt& binding) const { - const auto found = model_.hir.bindings.find(&binding); - return found == model_.hir.bindings.end() ? binding.name - : symbol_key(binding.name, found->second); -} - -std::string BorrowChecker::parameter_key(const ast::Param& parameter) const { - const auto found = model_.hir.params.find(¶meter); - return found == model_.hir.params.end() ? parameter.name - : symbol_key(parameter.name, found->second); -} - -std::string BorrowChecker::for_binding_key(const ast::ForStmt& loop) const { - const auto found = model_.hir.for_bindings.find(&loop); - return found == model_.hir.for_bindings.end() ? loop.name - : symbol_key(loop.name, found->second); -} - -std::vector BorrowChecker::reference_origins(const ast::Expr& expr, - const State& state) const { - std::vector result; - auto append = [&](std::vector more) { - for (auto& origin : more) { - const bool duplicate = - std::any_of(result.begin(), result.end(), [&](const RefOrigin& x) { - return same_origin(x, origin); - }); - if (!duplicate) - result.push_back(std::move(origin)); - } - }; - - if (expr.kind == ast::ExprKind::Borrow) { - const auto& borrow = static_cast(expr); - if (auto place = place_of(*borrow.target)) { - result.push_back({*place, expr.range, borrow.mut}); - return result; - } - result = reference_origins(*borrow.target, state); - for (auto& origin : result) - origin.mut = borrow.mut; - return result; - } - - if (auto place = place_of(expr)) { - for (const auto& [stored_place, origins] : state.ref_origins) { - if (*place == stored_place || is_strict_child(*place, stored_place) || - is_strict_child(stored_place, *place)) { - append(origins); - } - } - if (!result.empty()) - return result; - } - - switch (expr.kind) { - case ast::ExprKind::StructLiteral: - for (const auto& field : static_cast(expr).fields) - append(reference_origins(*field.value, state)); - break; - case ast::ExprKind::Unary: - append(reference_origins(*static_cast(expr).operand, state)); - break; - case ast::ExprKind::Call: { - bool result_can_contain_reference = false; - if (const auto type = model_.expr_types.find(&expr); type != model_.expr_types.end()) { - std::unordered_set visiting; - result_can_contain_reference = contains_reference(type->second, visiting); - } - if (result_can_contain_reference) { - const auto& call = static_cast(expr); - if (call.callee->kind == ast::ExprKind::Member) { - const auto& member = static_cast(*call.callee); - auto receiver_origins = reference_origins(*member.base, state); - if (!receiver_origins.empty()) { - append(std::move(receiver_origins)); - } else if (const auto receiver_place = place_of(*member.base)) { - Type receiver_type{}; - if (const auto found = model_.expr_types.find(member.base.get()); - found != model_.expr_types.end()) - receiver_type = found->second; - - Type owner = receiver_type; - if (owner.kind == TypeKind::Ref && owner.pointee) - owner = *owner.pointee; - const auto method = - model_.functions.find(owner.name + "::" + member.member); - if (method != model_.functions.end() && !method->second.params.empty() && - method->second.params.front().kind == TypeKind::Ref) { - if (receiver_type.kind == TypeKind::Ref) { - // A reference parameter can point outside the current frame. Keep - // that provenance external inside this function; the caller maps - // it back to its concrete argument when the call itself is bound. - result.push_back({"", - expr.range, - method->second.params.front().mut}); - } else { - result.push_back({*receiver_place, - expr.range, - method->second.params.front().mut}); - } - } - } - } - for (const auto& arg : call.args) - append(reference_origins(*arg, state)); - } - break; - } - case ast::ExprKind::Member: - append(reference_origins(*static_cast(expr).base, state)); - break; - case ast::ExprKind::Index: - append(reference_origins(*static_cast(expr).base, state)); - break; - case ast::ExprKind::Cast: - append(reference_origins(*static_cast(expr).value, state)); - break; - case ast::ExprKind::Await: - append(reference_origins(*static_cast(expr).value, state)); - break; - default: - break; - } - return result; -} - -bool BorrowChecker::contains_reference(const Type& type, - std::unordered_set& visiting) const { - if (type.kind == TypeKind::Ref) - return true; - if (type.kind == TypeKind::Generic) - return true; - if (type.kind != TypeKind::Named) - return false; - if (!visiting.insert(type.name).second) - return false; - - const auto structure = model_.structs.find(type.name); - if (structure == model_.structs.end()) { - visiting.erase(type.name); - return false; - } - - std::unordered_map substitutions; - for (std::size_t i = 0; i < structure->second.generic_names.size() && i < type.args.size(); - ++i) { - substitutions[structure->second.generic_names[i]] = type.args[i]; - } - - for (const auto& [name, field] : structure->second.fields) { - (void)name; - if (contains_reference(substitute_type(field, substitutions), visiting)) { - visiting.erase(type.name); - return true; - } - } - visiting.erase(type.name); - return false; -} - -Type BorrowChecker::type_of_place(std::string_view place, const State& state) const { - const std::string root = root_of(place); - Type type; - bool found = false; - for (auto it = state.scopes.rbegin(); it != state.scopes.rend(); ++it) { - const auto binding = it->find(root); - if (binding != it->end()) { - type = binding->second; - found = true; - break; - } - } - if (!found) { - const auto global = model_.globals.find(root); - if (global != model_.globals.end()) { - type = global->second.type; - found = true; - } - } - if (!found) - return {}; - - std::size_t pos = root.size(); - while (pos < place.size() && place[pos] == '.') { - ++pos; - const auto next = place.find('.', pos); - const std::string field( - place.substr(pos, next == std::string_view::npos ? place.size() - pos : next - pos)); - if (type.kind == TypeKind::Ref && type.pointee) - type = *type.pointee; - if (field == "[*]") { - if (type.kind == TypeKind::Named && (type.name == "Slice" || type.name == "SliceMut") && - !type.args.empty()) { - type = type.args[0]; - } else { - return {}; - } - } else { - const auto structure = model_.structs.find(type.name); - if (structure == model_.structs.end()) - return {}; - const auto member = structure->second.fields.find(field); - if (member == structure->second.fields.end()) - return {}; - std::unordered_map substitutions; - for (std::size_t i = 0; - i < structure->second.generic_names.size() && i < type.args.size(); - ++i) { - substitutions[structure->second.generic_names[i]] = type.args[i]; - } - type = substitute_type(member->second, substitutions); - } - if (next == std::string_view::npos) - break; - pos = next; - } - return type; -} - -bool BorrowChecker::unavailable(std::string_view place, const State& state) const { - return std::any_of(state.moved.begin(), state.moved.end(), [&](const std::string& moved) { - return overlaps(moved, place); - }); -} - -bool BorrowChecker::has_moved_ancestor(std::string_view place, const State& state) const { - return std::any_of(state.moved.begin(), state.moved.end(), [&](const std::string& moved) { - return moved != place && is_strict_child(moved, place); - }); -} - -bool BorrowChecker::is_stack_owned(std::string_view place, const State& state) const { - const std::string root = root_of(place); - for (const auto& scope : state.scopes) { - const auto binding = scope.find(root); - if (binding != scope.end()) { - // `ref T`/`mutref T` parameters and locals are stack bindings, but a field/index - // reached through them belongs to the referent, not to the reference slot itself. - // Borrowing the reference slot (`borrow r`) still correctly counts as stack-owned. - if (binding->second.kind == TypeKind::Ref && is_strict_child(root, place)) - return false; - return true; - } - } - return false; -} - -bool BorrowChecker::is_copy_type(const Type& type) const { - if (type.kind == TypeKind::Generic && copy_generics_.contains(type.name)) - return true; - if (type.is_copy()) - return true; - TraitSolver solver(model_); - return solver.satisfies(type, "Copy"); -} - -bool BorrowChecker::states_equivalent(const State& a, const State& b) const { - if (a.reachable != b.reachable || a.moved != b.moved || a.scopes.size() != b.scopes.size() || - a.borrows.size() != b.borrows.size() || a.ref_origins.size() != b.ref_origins.size()) { - return false; - } - for (const auto& borrow : a.borrows) { - if (std::none_of(b.borrows.begin(), b.borrows.end(), [&](const Borrow& other) { - return same_borrow(borrow, other); - })) { - return false; - } - } - for (const auto& [place, origins] : a.ref_origins) { - const auto found = b.ref_origins.find(place); - if (found == b.ref_origins.end() || found->second.size() != origins.size()) - return false; - for (const auto& origin : origins) { - if (std::none_of(found->second.begin(), - found->second.end(), - [&](const RefOrigin& other) { return same_origin(origin, other); })) { - return false; - } - } - } - return true; -} - -void BorrowChecker::mark_initialized(std::string_view place, State& state) { - for (auto it = state.moved.begin(); it != state.moved.end();) { - if (*it == place || is_strict_child(place, *it)) - it = state.moved.erase(it); - else - ++it; - } -} - -void BorrowChecker::kill_borrower(std::string_view borrower, State& state) { - state.borrows.erase( - std::remove_if(state.borrows.begin(), - state.borrows.end(), - [&](const Borrow& loan) { return loan.borrower == borrower; }), - state.borrows.end()); -} - -void BorrowChecker::kill_borrowers_under(std::string_view borrower, State& state) { - state.borrows.erase(std::remove_if(state.borrows.begin(), - state.borrows.end(), - [&](const Borrow& loan) { - return loan.borrower == borrower || - is_strict_child(borrower, loan.borrower); - }), - state.borrows.end()); -} - -void BorrowChecker::set_reference_origins(const std::string& place, - std::vector origins, - State& state) { - for (auto it = state.ref_origins.begin(); it != state.ref_origins.end();) { - if (it->first == place || is_strict_child(place, it->first)) - it = state.ref_origins.erase(it); - else - ++it; - } - if (origins.empty()) - return; - - std::vector unique; - for (auto& origin : origins) { - const bool duplicate = std::any_of(unique.begin(), unique.end(), [&](const RefOrigin& x) { - return same_origin(x, origin); - }); - if (!duplicate) - unique.push_back(std::move(origin)); - } - state.ref_origins[place] = std::move(unique); -} - -void BorrowChecker::promote_reference_value(const std::string& borrower, - const std::vector& origins, - State& state) { - if (origins.empty()) - return; - - state.borrows.erase( - std::remove_if(state.borrows.begin(), - state.borrows.end(), - [&](const Borrow& loan) { - if (loan.borrower != "") - return false; - return std::any_of( - origins.begin(), origins.end(), [&](const RefOrigin& o) { - return overlaps(loan.place, o.place); - }); - }), - state.borrows.end()); - - for (const auto& origin : origins) - begin_borrow(origin.place, borrower, origin.mut, origin.range, state); -} - -void BorrowChecker::bind_reference_expression(const std::string& destination, - const ast::Expr& expression, - State& state) { - if (expression.kind == ast::ExprKind::StructLiteral) { - const auto& literal = static_cast(expression); - for (const auto& field : literal.fields) - bind_reference_expression(destination + "." + field.name, *field.value, state); - return; - } - - if (const auto source = place_of(expression)) { - std::vector>> mapped; - for (const auto& [stored_place, origins] : state.ref_origins) { - if (stored_place == *source || is_strict_child(*source, stored_place)) { - const std::string suffix = stored_place.substr(source->size()); - mapped.push_back({destination + suffix, origins}); - } - } - if (!mapped.empty()) { - const Type source_type = type_of_place(*source, state); - const bool transfer = !is_copy_type(source_type); - if (transfer) { - kill_borrowers_under(*source, state); - for (auto it = state.ref_origins.begin(); it != state.ref_origins.end();) { - if (it->first == *source || is_strict_child(*source, it->first)) - it = state.ref_origins.erase(it); - else - ++it; - } - } - for (auto& [target, origins] : mapped) { - promote_reference_value(target, origins, state); - set_reference_origins(target, std::move(origins), state); - } - return; - } - } - - auto origins = reference_origins(expression, state); - if (!origins.empty()) { - promote_reference_value(destination, origins, state); - set_reference_origins(destination, std::move(origins), state); - } -} - -void BorrowChecker::access_place(const std::string& place, - const SourceRange& range, - Access access, - State& state) { - if (access == Access::Move) { - const Type type = type_of_place(place, state); - if (is_copy_type(type)) - access = Access::Read; - } - - if (access == Access::Write) { - if (has_moved_ancestor(place, state)) { - report_error(range, - "E0400", - "cannot initialize field/place '" + display_place(place) + - "' because an enclosing value was moved"); - return; - } - } else if (unavailable(place, state)) { - report_error(range, "E0400", "use of moved value/place '" + display_place(place) + "'"); - return; - } - - auto is_active_mut = [](const Borrow& loan) { - if (loan.kind == BorrowKind::TwoPhaseReserved) - return false; // reservation acts as shared - return loan.mut; - }; - auto is_active_exclusive = [](const Borrow& loan) { - if (loan.kind == BorrowKind::TwoPhaseReserved) - return false; - return loan.kind == BorrowKind::Mut || loan.kind == BorrowKind::TwoPhaseActivated; - }; - - if (access == Access::Read) { - for (const auto& loan : state.borrows) { - if (places_conflict(place, loan.place) && is_active_mut(loan)) { - report_error(range, - "E0406", - "cannot read '" + display_place(place) + - "' while mutable borrow of '" + display_place(loan.place) + - "' is active"); - } - } - return; - } - - if (access == Access::BorrowShared) { - for (const auto& loan : state.borrows) { - if (places_conflict(place, loan.place) && is_active_mut(loan)) { - report_error(range, - "E0401", - "cannot immutably borrow '" + display_place(place) + - "' while mutable borrow of '" + display_place(loan.place) + - "' is active"); - } - } - return; - } - - if (access == Access::TwoPhaseReserve) { - // Reservation phase: only conflicts with active exclusive borrows. - for (const auto& loan : state.borrows) { - if (places_conflict(place, loan.place) && is_active_exclusive(loan)) { - report_error(range, - "E0402", - "cannot reserve mutably '" + display_place(place) + - "' while exclusive borrow of '" + display_place(loan.place) + - "' is active"); - } - } - return; - } - - if (access == Access::BorrowMut) { - for (const auto& loan : state.borrows) { - if (places_conflict(place, loan.place)) { - // A two-phase reservation of the same place by the same - // statement is upgraded at activation, not a conflict here. - // Direct overlapping live loans are still errors. - report_error(range, - "E0402", - "cannot mutably borrow '" + display_place(place) + - "' while borrow of '" + display_place(loan.place) + "' is active"); - } - } - return; - } - - if (access == Access::Write || access == Access::Move) { - for (const auto& loan : state.borrows) { - if (places_conflict(place, loan.place)) { - report_error(range, - "E0403", - "cannot " + std::string(access == Access::Move ? "move" : "write") + - " '" + display_place(place) + "' while it is borrowed"); - } - } - } - - if (access == Access::Move) - state.moved.insert(place); -} - -void BorrowChecker::begin_borrow(const std::string& place, - std::string borrower, - bool mut, - const SourceRange& range, - State& state) { - access_place(place, range, mut ? Access::BorrowMut : Access::BorrowShared, state); - Borrow loan; - loan.place = place; - loan.borrower = std::move(borrower); - loan.mut = mut; - loan.kind = mut ? BorrowKind::Mut : BorrowKind::Shared; - loan.origin = range; - loan.reservation = range; - loan.activated = mut; - if (std::none_of(state.borrows.begin(), state.borrows.end(), [&](const Borrow& existing) { - return same_borrow(existing, loan); - })) { - state.borrows.push_back(std::move(loan)); - } -} - -void BorrowChecker::begin_two_phase_reservation(const std::string& place, - std::string borrower, - const SourceRange& reservation, - State& state) { - access_place(place, reservation, Access::TwoPhaseReserve, state); - Borrow loan; - loan.place = place; - loan.borrower = std::move(borrower); - loan.mut = true; - loan.kind = BorrowKind::TwoPhaseReserved; - loan.origin = reservation; - loan.reservation = reservation; - loan.activated = false; - if (std::none_of(state.borrows.begin(), state.borrows.end(), [&](const Borrow& existing) { - return same_borrow(existing, loan); - })) { - state.borrows.push_back(std::move(loan)); - } -} - -void BorrowChecker::activate_two_phase(const std::string& borrower, - const SourceRange& activation, - State& state) { - for (auto& loan : state.borrows) { - if (loan.borrower == borrower && loan.kind == BorrowKind::TwoPhaseReserved) { - // Activation: now acts as exclusive. Check against live shared. - for (const auto& other : state.borrows) { - if (&other == &loan) - continue; - if (places_conflict(loan.place, other.place)) { - report_error(activation, - "E0402", - "cannot activate mutable borrow of '" + display_place(loan.place) + - "' while borrow of '" + display_place(other.place) + - "' is active"); - } - } - loan.kind = BorrowKind::TwoPhaseActivated; - loan.origin = activation; - loan.activated = true; - } - } -} - -bool BorrowChecker::is_two_phase_method_receiver(const ast::Expr& base, - const ast::CallExpr& call) const { - if (call.callee->kind != ast::ExprKind::Member) - return false; - const auto& member = static_cast(*call.callee); - Type receiver{}; - if (const auto type = model_.expr_types.find(&base); type != model_.expr_types.end()) - receiver = type->second; - Type owner = receiver; - if (owner.kind == TypeKind::Ref && owner.pointee) - owner = *owner.pointee; - const auto method = model_.functions.find(owner.name + "::" + member.member); - if (method == model_.functions.end() || method->second.params.empty()) - return false; - const Type& self_type = method->second.params.front(); - return self_type.kind == TypeKind::Ref && self_type.mut; -} - -void BorrowChecker::inspect_call_with_two_phase(const ast::CallExpr& call, State& state) { - // Two-phase protocol for `receiver.mut_method(args...)`: - // reserve receiver (shared-like), inspect args, then activate. - const auto& member = static_cast(*call.callee); - const auto receiver_place = place_of(*member.base); - const std::string reservation_id = ""; - bool reserved = false; - if (receiver_place) { - begin_two_phase_reservation(*receiver_place, reservation_id, call.range, state); - reserved = true; - } else { - inspect_expr(*member.base, state, Access::BorrowShared); - } - // Args are inspected while the receiver reservation is shared-like, so - // `v.push(v.len())` is accepted: `v.len()` shared-borrows while reserved. - for (const auto& arg : call.args) { - const auto type = model_.expr_types.find(arg.get()); - Access arg_access = Access::Move; - if (type != model_.expr_types.end() && type->second.kind == TypeKind::Ref && - !type->second.mut) { - arg_access = Access::Read; - } - inspect_expr(*arg, state, arg_access); - } - if (reserved) { - activate_two_phase(reservation_id, call.range, state); - // Call-duration exclusive borrow ends with the call (unless the - // return value carries provenance, handled via reference_origins). - kill_borrower(reservation_id, state); - } - // Receiver provenance: method may return borrow of receiver. - // Keep existing reference_origins handling via caller. -} - -void BorrowChecker::collect_expr_uses(const ast::Expr& expr, - std::size_t index, - std::unordered_map& out) const { - switch (expr.kind) { - case ast::ExprKind::Name: - if (const auto place = place_of(expr)) - out[root_of(*place)] = index; - break; - case ast::ExprKind::StructLiteral: - for (const auto& field : static_cast(expr).fields) - collect_expr_uses(*field.value, index, out); - break; - case ast::ExprKind::Unary: - collect_expr_uses(*static_cast(expr).operand, index, out); - break; - case ast::ExprKind::Binary: { - const auto& binary = static_cast(expr); - collect_expr_uses(*binary.lhs, index, out); - collect_expr_uses(*binary.rhs, index, out); - break; - } - case ast::ExprKind::Borrow: - collect_expr_uses(*static_cast(expr).target, index, out); - break; - case ast::ExprKind::Call: { - const auto& call = static_cast(expr); - collect_expr_uses(*call.callee, index, out); - for (const auto& arg : call.args) - collect_expr_uses(*arg, index, out); - break; - } - case ast::ExprKind::Member: - collect_expr_uses(*static_cast(expr).base, index, out); - break; - case ast::ExprKind::Index: { - const auto& indexed = static_cast(expr); - collect_expr_uses(*indexed.base, index, out); - collect_expr_uses(*indexed.index, index, out); - break; - } - case ast::ExprKind::Await: - collect_expr_uses(*static_cast(expr).value, index, out); - break; - case ast::ExprKind::Cast: - collect_expr_uses(*static_cast(expr).value, index, out); - break; - case ast::ExprKind::Asm: - for (const auto& operand : static_cast(expr).operands) { - if (operand.value) - collect_expr_uses(*operand.value, index, out); - if (!operand.out_name.empty()) - out[operand.out_name] = index; - } - break; - default: - break; - } -} - -BorrowChecker::LiveSet BorrowChecker::expression_uses(const ast::Expr& expr) const { - std::unordered_map uses; - collect_expr_uses(expr, 0, uses); - LiveSet result; - for (const auto& [name, index] : uses) { - (void)index; - result.insert(name); - } - return result; -} - -void BorrowChecker::collect_place_uses(const ast::Expr& expr, LivePlaceSet& out) const { - if (auto place = place_of(expr)) { - out.insert(*place); - // Parent places are also considered used (shallow prefixes). - std::string cur = *place; - while (true) { - const auto dot = cur.rfind('.'); - if (dot == std::string::npos) - break; - cur = cur.substr(0, dot); - out.insert(cur); - } - } - switch (expr.kind) { - case ast::ExprKind::StructLiteral: - for (const auto& field : static_cast(expr).fields) - collect_place_uses(*field.value, out); - break; - case ast::ExprKind::Unary: - collect_place_uses(*static_cast(expr).operand, out); - break; - case ast::ExprKind::Binary: { - const auto& binary = static_cast(expr); - collect_place_uses(*binary.lhs, out); - collect_place_uses(*binary.rhs, out); - break; - } - case ast::ExprKind::Borrow: - collect_place_uses(*static_cast(expr).target, out); - break; - case ast::ExprKind::Call: { - const auto& call = static_cast(expr); - collect_place_uses(*call.callee, out); - for (const auto& arg : call.args) - collect_place_uses(*arg, out); - break; - } - case ast::ExprKind::Member: - collect_place_uses(*static_cast(expr).base, out); - break; - case ast::ExprKind::Index: { - const auto& indexed = static_cast(expr); - collect_place_uses(*indexed.base, out); - collect_place_uses(*indexed.index, out); - break; - } - case ast::ExprKind::Await: - collect_place_uses(*static_cast(expr).value, out); - break; - case ast::ExprKind::Cast: - collect_place_uses(*static_cast(expr).value, out); - break; - case ast::ExprKind::Asm: - for (const auto& operand : static_cast(expr).operands) { - if (operand.value) - collect_place_uses(*operand.value, out); - if (!operand.out_name.empty()) - out.insert(operand.out_name); - } - break; - default: - break; - } -} - -BorrowChecker::LivePlaceSet BorrowChecker::expression_place_uses(const ast::Expr& expr) const { - LivePlaceSet out; - collect_place_uses(expr, out); - return out; -} - -BorrowChecker::LiveSet BorrowChecker::liveness_block(const ast::BlockStmt& block, - const LiveSet& live_after, - const LiveSet& break_live, - const LiveSet& continue_live) { - LiveSet live = live_after; - for (auto it = block.stmts.rbegin(); it != block.stmts.rend(); ++it) - live = liveness_stmt(**it, live, break_live, continue_live); - return live; -} - -BorrowChecker::LiveSet BorrowChecker::liveness_stmt(const ast::Stmt& stmt, - const LiveSet& live_after, - const LiveSet& break_live, - const LiveSet& continue_live) { - live_after_[&stmt] = live_after; - LiveSet before = live_after; - auto add = [&](const LiveSet& values) { before.insert(values.begin(), values.end()); }; - - if (const auto* let = dynamic_cast(&stmt)) { - before.erase(binding_key(*let)); - if (let->init) - add(expression_uses(*let->init)); - } else if (const auto* assign = dynamic_cast(&stmt)) { - const auto target = place_of(*assign->target); - const bool whole_binding = target && *target == root_of(*target); - if (assign->op == "=" && whole_binding) - before.erase(*target); - else - add(expression_uses(*assign->target)); - add(expression_uses(*assign->value)); - } else if (const auto* expr = dynamic_cast(&stmt)) { - add(expression_uses(*expr->expr)); - } else if (const auto* ret = dynamic_cast(&stmt)) { - before.clear(); - if (ret->value) - add(expression_uses(*ret->value)); - } else if (const auto* block = dynamic_cast(&stmt)) { - before = liveness_block(*block, live_after, break_live, continue_live); - } else if (const auto* branch = dynamic_cast(&stmt)) { - const LiveSet left = - liveness_block(*branch->then_block, live_after, break_live, continue_live); - const LiveSet right = - branch->else_block - ? liveness_block(*branch->else_block, live_after, break_live, continue_live) - : live_after; - before = left; - before.insert(right.begin(), right.end()); - add(expression_uses(*branch->condition)); - } else if (const auto* while_stmt = dynamic_cast(&stmt)) { - LiveSet header = live_after; - bool converged = false; - for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { - const LiveSet body = liveness_block(*while_stmt->body, header, live_after, header); - LiveSet next = live_after; - next.insert(body.begin(), body.end()); - const LiveSet condition = expression_uses(*while_stmt->condition); - next.insert(condition.begin(), condition.end()); - if (next == header) { - converged = true; - break; - } - header = std::move(next); - } - if (!converged) - report_error(stmt.range, - "E0408", - "borrow liveness analysis did not converge; compilation stopped " - "conservatively"); - before = std::move(header); - } else if (const auto* for_stmt = dynamic_cast(&stmt)) { - LiveSet header = live_after; - bool converged = false; - for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { - LiveSet body = liveness_block(*for_stmt->body, header, live_after, header); - body.erase(for_binding_key(*for_stmt)); - LiveSet next = live_after; - next.insert(body.begin(), body.end()); - if (next == header) { - converged = true; - break; - } - header = std::move(next); - } - if (!converged) - report_error(stmt.range, - "E0408", - "borrow liveness analysis did not converge; compilation stopped " - "conservatively"); - before = std::move(header); - before.erase(for_binding_key(*for_stmt)); - add(expression_uses(*for_stmt->begin)); - add(expression_uses(*for_stmt->end)); - } else if (const auto* loop_stmt = dynamic_cast(&stmt)) { - LiveSet header = live_after; - bool converged = false; - for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { - LiveSet next = liveness_block(*loop_stmt->body, header, live_after, header); - if (next == header) { - converged = true; - break; - } - header = std::move(next); - } - if (!converged) - report_error(stmt.range, - "E0408", - "borrow liveness analysis did not converge; compilation stopped " - "conservatively"); - before = std::move(header); - } else if (dynamic_cast(&stmt)) { - before = break_live; - } else if (dynamic_cast(&stmt)) { - before = continue_live; - } else if (const auto* unsafe = dynamic_cast(&stmt)) { - before = liveness_block(*unsafe->body, live_after, break_live, continue_live); - } - - live_before_[&stmt] = before; - return before; -} - -void BorrowChecker::prepare_liveness(const ast::FunctionDecl& fn) { - live_before_.clear(); - live_after_.clear(); - live_places_before_.clear(); - live_places_after_.clear(); - if (!fn.body) - return; - const LiveSet empty; - (void)liveness_block(*fn.body, empty, empty, empty); - // Polonius-style second dimension: precise place liveness. Mirror the - // root pass but with full `a#1.b` strings so sibling-field loans expire - // independently and two-phase reservations are tracked precisely. - const LivePlaceSet empty_places; - (void)liveness_place_block(*fn.body, empty_places, empty_places, empty_places); -} - -BorrowChecker::LivePlaceSet BorrowChecker::liveness_place_block(const ast::BlockStmt& block, - const LivePlaceSet& live_after, - const LivePlaceSet& break_live, - const LivePlaceSet& continue_live) { - LivePlaceSet live = live_after; - for (auto it = block.stmts.rbegin(); it != block.stmts.rend(); ++it) - live = liveness_place_stmt(**it, live, break_live, continue_live); - return live; -} - -BorrowChecker::LivePlaceSet BorrowChecker::liveness_place_stmt(const ast::Stmt& stmt, - const LivePlaceSet& live_after, - const LivePlaceSet& break_live, - const LivePlaceSet& continue_live) { - live_places_after_[&stmt] = live_after; - LivePlaceSet before = live_after; - auto add = [&](const LivePlaceSet& values) { before.insert(values.begin(), values.end()); }; - - if (const auto* let = dynamic_cast(&stmt)) { - // Kill the defined place and all its children (strong update on - // whole-binding `=`), keep parent liveness for field updates. - std::vector to_erase; - const std::string key = binding_key(*let); - for (const auto& p : before) { - if (p == key || - (p.size() > key.size() && p.substr(0, key.size()) == key && p[key.size()] == '.')) - to_erase.push_back(p); - } - for (const auto& p : to_erase) - before.erase(p); - if (let->init) - add(expression_place_uses(*let->init)); - } else if (const auto* assign = dynamic_cast(&stmt)) { - const auto target = place_of(*assign->target); - const bool whole_binding = target && *target == root_of(*target); - if (assign->op == "=" && whole_binding && target) { - std::vector to_erase; - for (const auto& p : before) { - if (p == *target || - (p.size() > target->size() && p.substr(0, target->size()) == *target && - (*target).size() < p.size() && p[target->size()] == '.')) - to_erase.push_back(p); - } - for (const auto& p : to_erase) - before.erase(p); - } else { - if (assign->target) - add(expression_place_uses(*assign->target)); - } - add(expression_place_uses(*assign->value)); - } else if (const auto* expr = dynamic_cast(&stmt)) { - add(expression_place_uses(*expr->expr)); - } else if (const auto* ret = dynamic_cast(&stmt)) { - before.clear(); - if (ret->value) - add(expression_place_uses(*ret->value)); - } else if (const auto* block = dynamic_cast(&stmt)) { - before = liveness_place_block(*block, live_after, break_live, continue_live); - } else if (const auto* branch = dynamic_cast(&stmt)) { - const LivePlaceSet left = - liveness_place_block(*branch->then_block, live_after, break_live, continue_live); - const LivePlaceSet right = - branch->else_block - ? liveness_place_block(*branch->else_block, live_after, break_live, continue_live) - : live_after; - before = left; - before.insert(right.begin(), right.end()); - add(expression_place_uses(*branch->condition)); - } else if (const auto* while_stmt = dynamic_cast(&stmt)) { - LivePlaceSet header = live_after; - bool converged = false; - for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { - const LivePlaceSet body = - liveness_place_block(*while_stmt->body, header, live_after, header); - LivePlaceSet next = live_after; - next.insert(body.begin(), body.end()); - const LivePlaceSet condition = expression_place_uses(*while_stmt->condition); - next.insert(condition.begin(), condition.end()); - if (next == header) { - converged = true; - break; - } - header = std::move(next); - } - if (!converged) - report_error(stmt.range, - "E0408", - "borrow liveness analysis did not converge; compilation stopped " - "conservatively"); - before = std::move(header); - } else if (const auto* for_stmt = dynamic_cast(&stmt)) { - LivePlaceSet header = live_after; - bool converged = false; - for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { - LivePlaceSet body = liveness_place_block(*for_stmt->body, header, live_after, header); - // Kill loop variable places in body liveness. - const std::string loop_key = for_binding_key(*for_stmt); - std::vector to_erase; - for (const auto& p : body) { - if (p == loop_key || root_of(p) == loop_key) - to_erase.push_back(p); - } - for (const auto& p : to_erase) - body.erase(p); - LivePlaceSet next = live_after; - next.insert(body.begin(), body.end()); - if (next == header) { - converged = true; - break; - } - header = std::move(next); - } - if (!converged) - report_error(stmt.range, - "E0408", - "borrow liveness analysis did not converge; compilation stopped " - "conservatively"); - before = std::move(header); - { - const std::string loop_key = for_binding_key(*for_stmt); - std::vector to_erase; - for (const auto& p : before) { - if (p == loop_key || root_of(p) == loop_key) - to_erase.push_back(p); - } - for (const auto& p : to_erase) - before.erase(p); - } - add(expression_place_uses(*for_stmt->begin)); - add(expression_place_uses(*for_stmt->end)); - } else if (const auto* loop_stmt = dynamic_cast(&stmt)) { - LivePlaceSet header = live_after; - bool converged = false; - for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { - LivePlaceSet next = liveness_place_block(*loop_stmt->body, header, live_after, header); - if (next == header) { - converged = true; - break; - } - header = std::move(next); - } - if (!converged) - report_error(stmt.range, - "E0408", - "borrow liveness analysis did not converge; compilation stopped " - "conservatively"); - before = std::move(header); - } else if (dynamic_cast(&stmt)) { - before = break_live; - } else if (dynamic_cast(&stmt)) { - before = continue_live; - } else if (const auto* unsafe = dynamic_cast(&stmt)) { - before = liveness_place_block(*unsafe->body, live_after, break_live, continue_live); - } - - live_places_before_[&stmt] = before; - return before; -} - -void BorrowChecker::expire_dead_loans(const ast::Stmt& stmt, State& state) { - const auto live_it = live_before_.find(&stmt); - const auto live_places_it = live_places_before_.find(&stmt); - const LiveSet empty_roots; - const LivePlaceSet empty_places; - const LiveSet& live_roots = live_it == live_before_.end() ? empty_roots : live_it->second; - const LivePlaceSet& live_places = - live_places_it == live_places_before_.end() ? empty_places : live_places_it->second; - state.borrows.erase(std::remove_if(state.borrows.begin(), - state.borrows.end(), - [&](const Borrow& loan) { - if (loan.borrower == "") - return true; - // Two-phase reservations with `` borrower - // are statement-local; keep until activation. - if (loan.borrower.starts_with("& accumulator, const State& state) const { - if (!state.reachable) - return; - if (!accumulator) { - accumulator = state; - return; - } - State merged; - merge_states(merged, *accumulator, state); - accumulator = std::move(merged); -} - -void BorrowChecker::truncate_scopes(State& state, std::size_t depth) { - while (state.scopes.size() > depth) { - std::unordered_set locals; - for (const auto& [name, type] : state.scopes.back()) { - (void)type; - locals.insert(name); - } - - for (const auto& [borrower, origins] : state.ref_origins) { - if (locals.contains(root_of(borrower))) - continue; - for (const auto& origin : origins) { - if (locals.contains(root_of(origin.place))) { - report_error(origin.range, - "E0405", - "reference stored in '" + display_place(borrower) + - "' outlives local place '" + display_place(origin.place) + - "'"); - } - } - } - - state.scopes.pop_back(); - state.borrows.erase(std::remove_if(state.borrows.begin(), - state.borrows.end(), - [&](const Borrow& loan) { - return locals.contains(root_of(loan.borrower)); - }), - state.borrows.end()); - for (auto it = state.moved.begin(); it != state.moved.end();) { - if (locals.contains(root_of(*it))) - it = state.moved.erase(it); - else - ++it; - } - for (auto it = state.ref_origins.begin(); it != state.ref_origins.end();) { - if (locals.contains(root_of(it->first))) - it = state.ref_origins.erase(it); - else - ++it; - } - } -} - -void BorrowChecker::inspect_expr(const ast::Expr& expr, State& state, Access access) { - if (!state.reachable) - return; - if (auto place = place_of(expr)) { - access_place(*place, expr.range, access, state); - return; - } - - switch (expr.kind) { - case ast::ExprKind::StructLiteral: - for (const auto& field : static_cast(expr).fields) - inspect_expr(*field.value, state, Access::Move); - break; - case ast::ExprKind::Unary: { - const auto& unary = static_cast(expr); - inspect_expr(*unary.operand, - state, - unary.op == "*" ? Access::Read - : (unary.op == "move" ? Access::Move : access)); - break; - } - case ast::ExprKind::Binary: { - const auto& binary = static_cast(expr); - inspect_expr(*binary.lhs, state, Access::Move); - inspect_expr(*binary.rhs, state, Access::Move); - break; - } - case ast::ExprKind::Borrow: { - const auto& borrow = static_cast(expr); - if (auto place = place_of(*borrow.target)) { - begin_borrow(*place, "", borrow.mut, expr.range, state); - } else { - const auto origins = reference_origins(*borrow.target, state); - if (!origins.empty()) { - for (const auto& origin : origins) - begin_borrow(origin.place, "", borrow.mut, expr.range, state); - } else { - inspect_expr(*borrow.target, state, Access::Read); - } - } - break; - } - case ast::ExprKind::Call: { - const auto& call = static_cast(expr); - if (call.callee->kind == ast::ExprKind::Member) { - const auto& member = static_cast(*call.callee); - if (is_two_phase_method_receiver(*member.base, call)) { - inspect_call_with_two_phase(call, state); - break; - } - Type receiver{}; - if (const auto type = model_.expr_types.find(member.base.get()); - type != model_.expr_types.end()) { - receiver = type->second; - } - Type owner = receiver; - if (owner.kind == TypeKind::Ref && owner.pointee) - owner = *owner.pointee; - const auto method = model_.functions.find(owner.name + "::" + member.member); - if (method != model_.functions.end() && !method->second.params.empty()) { - Access receiver_access = Access::Move; - const Type& self_type = method->second.params.front(); - if (self_type.kind == TypeKind::Ref) - receiver_access = self_type.mut ? Access::BorrowMut : Access::BorrowShared; - inspect_expr(*member.base, state, receiver_access); - } else { - inspect_expr(*member.base, state, Access::Move); - } - } - // Two-phase for function args with `borrow mut` reborrows - // (e.g. `replace(r, vec![r.len()])`): reserve all `borrow mut` - // args as shared-like, inspect remaining args, then activate. - // Detect `borrow mut ` args directly. - std::vector reserved_places; - std::vector reservation_ids; - for (std::size_t i = 0; i < call.args.size(); ++i) { - const auto& arg = call.args[i]; - if (arg->kind == ast::ExprKind::Borrow) { - const auto& b = static_cast(*arg); - if (b.mut) { - if (auto place = place_of(*b.target)) { - const std::string rid = - ""; - begin_two_phase_reservation(*place, rid, call.range, state); - reserved_places.push_back(*place); - reservation_ids.push_back(rid); - } - } - } - } - if (!reserved_places.empty()) { - for (const auto& arg : call.args) { - // Skip already-reserved `borrow mut` args (reservation - // covers them); inspect others while reserved. - if (arg->kind == ast::ExprKind::Borrow && - static_cast(*arg).mut) - continue; - const auto type = model_.expr_types.find(arg.get()); - Access arg_access = Access::Move; - if (type != model_.expr_types.end() && type->second.kind == TypeKind::Ref && - !type->second.mut) { - arg_access = Access::Read; - } - inspect_expr(*arg, state, arg_access); - } - for (const auto& rid : reservation_ids) - activate_two_phase(rid, call.range, state); - for (const auto& rid : reservation_ids) - kill_borrower(rid, state); - break; - } - for (const auto& arg : call.args) { - const auto type = model_.expr_types.find(arg.get()); - Access arg_access = Access::Move; - if (type != model_.expr_types.end() && type->second.kind == TypeKind::Ref && - !type->second.mut) { - arg_access = Access::Read; - } - inspect_expr(*arg, state, arg_access); - } - break; - } - case ast::ExprKind::Index: { - const auto& indexed = static_cast(expr); - inspect_expr(*indexed.base, state, Access::Read); - inspect_expr(*indexed.index, state, Access::Move); - break; - } - case ast::ExprKind::Await: - if (state.async_context) { - for (const auto& loan : state.borrows) { - if (loan.borrower != "" && is_stack_owned(loan.place, state)) { - report_error(expr.range, - "E0407", - "safe reference to stack-owned place '" + - display_place(loan.place) + - "' cannot remain live across await"); - } - } - } - inspect_expr(*static_cast(expr).value, state, Access::Move); - break; - case ast::ExprKind::Cast: - inspect_expr(*static_cast(expr).value, state, Access::Move); - break; - case ast::ExprKind::Asm: - for (const auto& operand : static_cast(expr).operands) { - if (operand.value) - inspect_expr(*operand.value, state, Access::Read); - if (!operand.out_name.empty()) - access_place(operand.out_name, operand.range, Access::Write, state); - } - break; - default: - break; - } -} +using namespace borrow_detail; void BorrowChecker::check_stmt(const ast::Stmt& stmt, State& state) { if (!state.reachable) diff --git a/src/borrow_flow.cpp b/src/borrow_flow.cpp new file mode 100644 index 0000000..ab0ed46 --- /dev/null +++ b/src/borrow_flow.cpp @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "borrow_internal.hpp" +#include "uinx/borrow.hpp" + +#include +#include + +namespace uinx { +using namespace borrow_detail; + +void BorrowChecker::merge_states(State& out, const State& left, const State& right) const { + if (!left.reachable && !right.reachable) { + out = left; + out.reachable = false; + return; + } + if (!left.reachable) { + out = right; + return; + } + if (!right.reachable) { + out = left; + return; + } + + out = left; + out.reachable = true; + out.moved.insert(right.moved.begin(), right.moved.end()); + for (const auto& loan : right.borrows) { + if (std::none_of(out.borrows.begin(), out.borrows.end(), [&](const Borrow& existing) { + return same_borrow(existing, loan); + })) { + out.borrows.push_back(loan); + } + } + for (const auto& [place, origins] : right.ref_origins) { + auto& merged = out.ref_origins[place]; + for (const auto& origin : origins) { + if (std::none_of(merged.begin(), merged.end(), [&](const RefOrigin& existing) { + return same_origin(existing, origin); + })) { + merged.push_back(origin); + } + } + } +} + +void BorrowChecker::merge_into(std::optional& accumulator, const State& state) const { + if (!state.reachable) + return; + if (!accumulator) { + accumulator = state; + return; + } + State merged; + merge_states(merged, *accumulator, state); + accumulator = std::move(merged); +} + +void BorrowChecker::truncate_scopes(State& state, std::size_t depth) { + while (state.scopes.size() > depth) { + std::unordered_set locals; + for (const auto& [name, type] : state.scopes.back()) { + (void)type; + locals.insert(name); + } + + for (const auto& [borrower, origins] : state.ref_origins) { + if (locals.contains(root_of(borrower))) + continue; + for (const auto& origin : origins) { + if (locals.contains(root_of(origin.place))) { + report_error(origin.range, + "E0405", + "reference stored in '" + display_place(borrower) + + "' outlives local place '" + display_place(origin.place) + + "'"); + } + } + } + + state.scopes.pop_back(); + state.borrows.erase(std::remove_if(state.borrows.begin(), + state.borrows.end(), + [&](const Borrow& loan) { + return locals.contains(root_of(loan.borrower)); + }), + state.borrows.end()); + for (auto it = state.moved.begin(); it != state.moved.end();) { + if (locals.contains(root_of(*it))) + it = state.moved.erase(it); + else + ++it; + } + for (auto it = state.ref_origins.begin(); it != state.ref_origins.end();) { + if (locals.contains(root_of(it->first))) + it = state.ref_origins.erase(it); + else + ++it; + } + } +} + +void BorrowChecker::inspect_expr(const ast::Expr& expr, State& state, Access access) { + if (!state.reachable) + return; + if (auto place = place_of(expr)) { + access_place(*place, expr.range, access, state); + return; + } + + switch (expr.kind) { + case ast::ExprKind::StructLiteral: + for (const auto& field : static_cast(expr).fields) + inspect_expr(*field.value, state, Access::Move); + break; + case ast::ExprKind::Unary: { + const auto& unary = static_cast(expr); + inspect_expr(*unary.operand, + state, + unary.op == "*" ? Access::Read + : (unary.op == "move" ? Access::Move : access)); + break; + } + case ast::ExprKind::Binary: { + const auto& binary = static_cast(expr); + inspect_expr(*binary.lhs, state, Access::Move); + inspect_expr(*binary.rhs, state, Access::Move); + break; + } + case ast::ExprKind::Borrow: { + const auto& borrow = static_cast(expr); + if (auto place = place_of(*borrow.target)) { + begin_borrow(*place, "", borrow.mut, expr.range, state); + } else { + const auto origins = reference_origins(*borrow.target, state); + if (!origins.empty()) { + for (const auto& origin : origins) + begin_borrow(origin.place, "", borrow.mut, expr.range, state); + } else { + inspect_expr(*borrow.target, state, Access::Read); + } + } + break; + } + case ast::ExprKind::Call: { + const auto& call = static_cast(expr); + if (call.callee->kind == ast::ExprKind::Member) { + const auto& member = static_cast(*call.callee); + if (is_two_phase_method_receiver(*member.base, call)) { + inspect_call_with_two_phase(call, state); + break; + } + Type receiver{}; + if (const auto type = model_.expr_types.find(member.base.get()); + type != model_.expr_types.end()) { + receiver = type->second; + } + Type owner = receiver; + if (owner.kind == TypeKind::Ref && owner.pointee) + owner = *owner.pointee; + const auto method = model_.functions.find(owner.name + "::" + member.member); + if (method != model_.functions.end() && !method->second.params.empty()) { + Access receiver_access = Access::Move; + const Type& self_type = method->second.params.front(); + if (self_type.kind == TypeKind::Ref) + receiver_access = self_type.mut ? Access::BorrowMut : Access::BorrowShared; + inspect_expr(*member.base, state, receiver_access); + } else { + inspect_expr(*member.base, state, Access::Move); + } + } + // Two-phase for function args with `borrow mut` reborrows + // (e.g. `replace(r, vec![r.len()])`): reserve all `borrow mut` + // args as shared-like, inspect remaining args, then activate. + // Detect `borrow mut ` args directly. + std::vector reserved_places; + std::vector reservation_ids; + for (std::size_t i = 0; i < call.args.size(); ++i) { + const auto& arg = call.args[i]; + if (arg->kind == ast::ExprKind::Borrow) { + const auto& b = static_cast(*arg); + if (b.mut) { + if (auto place = place_of(*b.target)) { + const std::string rid = + ""; + begin_two_phase_reservation(*place, rid, call.range, state); + reserved_places.push_back(*place); + reservation_ids.push_back(rid); + } + } + } + } + if (!reserved_places.empty()) { + for (const auto& arg : call.args) { + // Skip already-reserved `borrow mut` args (reservation + // covers them); inspect others while reserved. + if (arg->kind == ast::ExprKind::Borrow && + static_cast(*arg).mut) + continue; + const auto type = model_.expr_types.find(arg.get()); + Access arg_access = Access::Move; + if (type != model_.expr_types.end() && type->second.kind == TypeKind::Ref && + !type->second.mut) { + arg_access = Access::Read; + } + inspect_expr(*arg, state, arg_access); + } + for (const auto& rid : reservation_ids) + activate_two_phase(rid, call.range, state); + for (const auto& rid : reservation_ids) + kill_borrower(rid, state); + break; + } + for (const auto& arg : call.args) { + const auto type = model_.expr_types.find(arg.get()); + Access arg_access = Access::Move; + if (type != model_.expr_types.end() && type->second.kind == TypeKind::Ref && + !type->second.mut) { + arg_access = Access::Read; + } + inspect_expr(*arg, state, arg_access); + } + break; + } + case ast::ExprKind::Index: { + const auto& indexed = static_cast(expr); + inspect_expr(*indexed.base, state, Access::Read); + inspect_expr(*indexed.index, state, Access::Move); + break; + } + case ast::ExprKind::Await: + if (state.async_context) { + for (const auto& loan : state.borrows) { + if (loan.borrower != "" && is_stack_owned(loan.place, state)) { + report_error(expr.range, + "E0407", + "safe reference to stack-owned place '" + + display_place(loan.place) + + "' cannot remain live across await"); + } + } + } + inspect_expr(*static_cast(expr).value, state, Access::Move); + break; + case ast::ExprKind::Cast: + inspect_expr(*static_cast(expr).value, state, Access::Move); + break; + case ast::ExprKind::Asm: + for (const auto& operand : static_cast(expr).operands) { + if (operand.value) + inspect_expr(*operand.value, state, Access::Read); + if (!operand.out_name.empty()) + access_place(operand.out_name, operand.range, Access::Write, state); + } + break; + default: + break; + } +} +} // namespace uinx diff --git a/src/borrow_internal.hpp b/src/borrow_internal.hpp new file mode 100644 index 0000000..88201c7 --- /dev/null +++ b/src/borrow_internal.hpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +// Internal helpers shared by the translation units split out of borrow.cpp. + +#pragma once + +#include "uinx/borrow.hpp" + +#include +#include + +namespace uinx::borrow_detail { +inline constexpr std::size_t kMaxDataflowIterations = 4096; + +inline std::string root_of(std::string_view place) { + const auto dot = place.find('.'); + return std::string(place.substr(0, dot)); +} + +inline bool is_strict_child(std::string_view parent, std::string_view child) { + return child.size() > parent.size() && child.substr(0, parent.size()) == parent && + child[parent.size()] == '.'; +} + +inline std::string symbol_key(std::string_view name, hir::SymbolId id) { + return std::string(name) + "#" + std::to_string(id); +} + +inline std::string display_place(std::string_view place) { + if (place.starts_with(" split_place(std::string_view place) { + std::vector parts; + std::size_t start = 0; + while (true) { + const auto dot = place.find('.', start); + if (dot == std::string_view::npos) { + parts.push_back(std::string(place.substr(start))); + break; + } + parts.push_back(std::string(place.substr(start, dot - start))); + start = dot + 1; + } + return parts; +} +} // namespace uinx::borrow_detail diff --git a/src/borrow_liveness.cpp b/src/borrow_liveness.cpp new file mode 100644 index 0000000..a1c09d3 --- /dev/null +++ b/src/borrow_liveness.cpp @@ -0,0 +1,472 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "borrow_internal.hpp" +#include "uinx/borrow.hpp" + +#include +#include + +namespace uinx { +using namespace borrow_detail; + +void BorrowChecker::collect_expr_uses(const ast::Expr& expr, + std::size_t index, + std::unordered_map& out) const { + switch (expr.kind) { + case ast::ExprKind::Name: + if (const auto place = place_of(expr)) + out[root_of(*place)] = index; + break; + case ast::ExprKind::StructLiteral: + for (const auto& field : static_cast(expr).fields) + collect_expr_uses(*field.value, index, out); + break; + case ast::ExprKind::Unary: + collect_expr_uses(*static_cast(expr).operand, index, out); + break; + case ast::ExprKind::Binary: { + const auto& binary = static_cast(expr); + collect_expr_uses(*binary.lhs, index, out); + collect_expr_uses(*binary.rhs, index, out); + break; + } + case ast::ExprKind::Borrow: + collect_expr_uses(*static_cast(expr).target, index, out); + break; + case ast::ExprKind::Call: { + const auto& call = static_cast(expr); + collect_expr_uses(*call.callee, index, out); + for (const auto& arg : call.args) + collect_expr_uses(*arg, index, out); + break; + } + case ast::ExprKind::Member: + collect_expr_uses(*static_cast(expr).base, index, out); + break; + case ast::ExprKind::Index: { + const auto& indexed = static_cast(expr); + collect_expr_uses(*indexed.base, index, out); + collect_expr_uses(*indexed.index, index, out); + break; + } + case ast::ExprKind::Await: + collect_expr_uses(*static_cast(expr).value, index, out); + break; + case ast::ExprKind::Cast: + collect_expr_uses(*static_cast(expr).value, index, out); + break; + case ast::ExprKind::Asm: + for (const auto& operand : static_cast(expr).operands) { + if (operand.value) + collect_expr_uses(*operand.value, index, out); + if (!operand.out_name.empty()) + out[operand.out_name] = index; + } + break; + default: + break; + } +} + +BorrowChecker::LiveSet BorrowChecker::expression_uses(const ast::Expr& expr) const { + std::unordered_map uses; + collect_expr_uses(expr, 0, uses); + LiveSet result; + for (const auto& [name, index] : uses) { + (void)index; + result.insert(name); + } + return result; +} + +void BorrowChecker::collect_place_uses(const ast::Expr& expr, LivePlaceSet& out) const { + if (auto place = place_of(expr)) { + out.insert(*place); + // Parent places are also considered used (shallow prefixes). + std::string cur = *place; + while (true) { + const auto dot = cur.rfind('.'); + if (dot == std::string::npos) + break; + cur = cur.substr(0, dot); + out.insert(cur); + } + } + switch (expr.kind) { + case ast::ExprKind::StructLiteral: + for (const auto& field : static_cast(expr).fields) + collect_place_uses(*field.value, out); + break; + case ast::ExprKind::Unary: + collect_place_uses(*static_cast(expr).operand, out); + break; + case ast::ExprKind::Binary: { + const auto& binary = static_cast(expr); + collect_place_uses(*binary.lhs, out); + collect_place_uses(*binary.rhs, out); + break; + } + case ast::ExprKind::Borrow: + collect_place_uses(*static_cast(expr).target, out); + break; + case ast::ExprKind::Call: { + const auto& call = static_cast(expr); + collect_place_uses(*call.callee, out); + for (const auto& arg : call.args) + collect_place_uses(*arg, out); + break; + } + case ast::ExprKind::Member: + collect_place_uses(*static_cast(expr).base, out); + break; + case ast::ExprKind::Index: { + const auto& indexed = static_cast(expr); + collect_place_uses(*indexed.base, out); + collect_place_uses(*indexed.index, out); + break; + } + case ast::ExprKind::Await: + collect_place_uses(*static_cast(expr).value, out); + break; + case ast::ExprKind::Cast: + collect_place_uses(*static_cast(expr).value, out); + break; + case ast::ExprKind::Asm: + for (const auto& operand : static_cast(expr).operands) { + if (operand.value) + collect_place_uses(*operand.value, out); + if (!operand.out_name.empty()) + out.insert(operand.out_name); + } + break; + default: + break; + } +} + +BorrowChecker::LivePlaceSet BorrowChecker::expression_place_uses(const ast::Expr& expr) const { + LivePlaceSet out; + collect_place_uses(expr, out); + return out; +} + +BorrowChecker::LiveSet BorrowChecker::liveness_block(const ast::BlockStmt& block, + const LiveSet& live_after, + const LiveSet& break_live, + const LiveSet& continue_live) { + LiveSet live = live_after; + for (auto it = block.stmts.rbegin(); it != block.stmts.rend(); ++it) + live = liveness_stmt(**it, live, break_live, continue_live); + return live; +} + +BorrowChecker::LiveSet BorrowChecker::liveness_stmt(const ast::Stmt& stmt, + const LiveSet& live_after, + const LiveSet& break_live, + const LiveSet& continue_live) { + live_after_[&stmt] = live_after; + LiveSet before = live_after; + auto add = [&](const LiveSet& values) { before.insert(values.begin(), values.end()); }; + + if (const auto* let = dynamic_cast(&stmt)) { + before.erase(binding_key(*let)); + if (let->init) + add(expression_uses(*let->init)); + } else if (const auto* assign = dynamic_cast(&stmt)) { + const auto target = place_of(*assign->target); + const bool whole_binding = target && *target == root_of(*target); + if (assign->op == "=" && whole_binding) + before.erase(*target); + else + add(expression_uses(*assign->target)); + add(expression_uses(*assign->value)); + } else if (const auto* expr = dynamic_cast(&stmt)) { + add(expression_uses(*expr->expr)); + } else if (const auto* ret = dynamic_cast(&stmt)) { + before.clear(); + if (ret->value) + add(expression_uses(*ret->value)); + } else if (const auto* block = dynamic_cast(&stmt)) { + before = liveness_block(*block, live_after, break_live, continue_live); + } else if (const auto* branch = dynamic_cast(&stmt)) { + const LiveSet left = + liveness_block(*branch->then_block, live_after, break_live, continue_live); + const LiveSet right = + branch->else_block + ? liveness_block(*branch->else_block, live_after, break_live, continue_live) + : live_after; + before = left; + before.insert(right.begin(), right.end()); + add(expression_uses(*branch->condition)); + } else if (const auto* while_stmt = dynamic_cast(&stmt)) { + LiveSet header = live_after; + bool converged = false; + for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { + const LiveSet body = liveness_block(*while_stmt->body, header, live_after, header); + LiveSet next = live_after; + next.insert(body.begin(), body.end()); + const LiveSet condition = expression_uses(*while_stmt->condition); + next.insert(condition.begin(), condition.end()); + if (next == header) { + converged = true; + break; + } + header = std::move(next); + } + if (!converged) + report_error(stmt.range, + "E0408", + "borrow liveness analysis did not converge; compilation stopped " + "conservatively"); + before = std::move(header); + } else if (const auto* for_stmt = dynamic_cast(&stmt)) { + LiveSet header = live_after; + bool converged = false; + for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { + LiveSet body = liveness_block(*for_stmt->body, header, live_after, header); + body.erase(for_binding_key(*for_stmt)); + LiveSet next = live_after; + next.insert(body.begin(), body.end()); + if (next == header) { + converged = true; + break; + } + header = std::move(next); + } + if (!converged) + report_error(stmt.range, + "E0408", + "borrow liveness analysis did not converge; compilation stopped " + "conservatively"); + before = std::move(header); + before.erase(for_binding_key(*for_stmt)); + add(expression_uses(*for_stmt->begin)); + add(expression_uses(*for_stmt->end)); + } else if (const auto* loop_stmt = dynamic_cast(&stmt)) { + LiveSet header = live_after; + bool converged = false; + for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { + LiveSet next = liveness_block(*loop_stmt->body, header, live_after, header); + if (next == header) { + converged = true; + break; + } + header = std::move(next); + } + if (!converged) + report_error(stmt.range, + "E0408", + "borrow liveness analysis did not converge; compilation stopped " + "conservatively"); + before = std::move(header); + } else if (dynamic_cast(&stmt)) { + before = break_live; + } else if (dynamic_cast(&stmt)) { + before = continue_live; + } else if (const auto* unsafe = dynamic_cast(&stmt)) { + before = liveness_block(*unsafe->body, live_after, break_live, continue_live); + } + + live_before_[&stmt] = before; + return before; +} + +void BorrowChecker::prepare_liveness(const ast::FunctionDecl& fn) { + live_before_.clear(); + live_after_.clear(); + live_places_before_.clear(); + live_places_after_.clear(); + if (!fn.body) + return; + const LiveSet empty; + (void)liveness_block(*fn.body, empty, empty, empty); + // Polonius-style second dimension: precise place liveness. Mirror the + // root pass but with full `a#1.b` strings so sibling-field loans expire + // independently and two-phase reservations are tracked precisely. + const LivePlaceSet empty_places; + (void)liveness_place_block(*fn.body, empty_places, empty_places, empty_places); +} + +BorrowChecker::LivePlaceSet BorrowChecker::liveness_place_block(const ast::BlockStmt& block, + const LivePlaceSet& live_after, + const LivePlaceSet& break_live, + const LivePlaceSet& continue_live) { + LivePlaceSet live = live_after; + for (auto it = block.stmts.rbegin(); it != block.stmts.rend(); ++it) + live = liveness_place_stmt(**it, live, break_live, continue_live); + return live; +} + +BorrowChecker::LivePlaceSet BorrowChecker::liveness_place_stmt(const ast::Stmt& stmt, + const LivePlaceSet& live_after, + const LivePlaceSet& break_live, + const LivePlaceSet& continue_live) { + live_places_after_[&stmt] = live_after; + LivePlaceSet before = live_after; + auto add = [&](const LivePlaceSet& values) { before.insert(values.begin(), values.end()); }; + + if (const auto* let = dynamic_cast(&stmt)) { + // Kill the defined place and all its children (strong update on + // whole-binding `=`), keep parent liveness for field updates. + std::vector to_erase; + const std::string key = binding_key(*let); + for (const auto& p : before) { + if (p == key || + (p.size() > key.size() && p.substr(0, key.size()) == key && p[key.size()] == '.')) + to_erase.push_back(p); + } + for (const auto& p : to_erase) + before.erase(p); + if (let->init) + add(expression_place_uses(*let->init)); + } else if (const auto* assign = dynamic_cast(&stmt)) { + const auto target = place_of(*assign->target); + const bool whole_binding = target && *target == root_of(*target); + if (assign->op == "=" && whole_binding && target) { + std::vector to_erase; + for (const auto& p : before) { + if (p == *target || + (p.size() > target->size() && p.substr(0, target->size()) == *target && + (*target).size() < p.size() && p[target->size()] == '.')) + to_erase.push_back(p); + } + for (const auto& p : to_erase) + before.erase(p); + } else { + if (assign->target) + add(expression_place_uses(*assign->target)); + } + add(expression_place_uses(*assign->value)); + } else if (const auto* expr = dynamic_cast(&stmt)) { + add(expression_place_uses(*expr->expr)); + } else if (const auto* ret = dynamic_cast(&stmt)) { + before.clear(); + if (ret->value) + add(expression_place_uses(*ret->value)); + } else if (const auto* block = dynamic_cast(&stmt)) { + before = liveness_place_block(*block, live_after, break_live, continue_live); + } else if (const auto* branch = dynamic_cast(&stmt)) { + const LivePlaceSet left = + liveness_place_block(*branch->then_block, live_after, break_live, continue_live); + const LivePlaceSet right = + branch->else_block + ? liveness_place_block(*branch->else_block, live_after, break_live, continue_live) + : live_after; + before = left; + before.insert(right.begin(), right.end()); + add(expression_place_uses(*branch->condition)); + } else if (const auto* while_stmt = dynamic_cast(&stmt)) { + LivePlaceSet header = live_after; + bool converged = false; + for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { + const LivePlaceSet body = + liveness_place_block(*while_stmt->body, header, live_after, header); + LivePlaceSet next = live_after; + next.insert(body.begin(), body.end()); + const LivePlaceSet condition = expression_place_uses(*while_stmt->condition); + next.insert(condition.begin(), condition.end()); + if (next == header) { + converged = true; + break; + } + header = std::move(next); + } + if (!converged) + report_error(stmt.range, + "E0408", + "borrow liveness analysis did not converge; compilation stopped " + "conservatively"); + before = std::move(header); + } else if (const auto* for_stmt = dynamic_cast(&stmt)) { + LivePlaceSet header = live_after; + bool converged = false; + for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { + LivePlaceSet body = liveness_place_block(*for_stmt->body, header, live_after, header); + // Kill loop variable places in body liveness. + const std::string loop_key = for_binding_key(*for_stmt); + std::vector to_erase; + for (const auto& p : body) { + if (p == loop_key || root_of(p) == loop_key) + to_erase.push_back(p); + } + for (const auto& p : to_erase) + body.erase(p); + LivePlaceSet next = live_after; + next.insert(body.begin(), body.end()); + if (next == header) { + converged = true; + break; + } + header = std::move(next); + } + if (!converged) + report_error(stmt.range, + "E0408", + "borrow liveness analysis did not converge; compilation stopped " + "conservatively"); + before = std::move(header); + { + const std::string loop_key = for_binding_key(*for_stmt); + std::vector to_erase; + for (const auto& p : before) { + if (p == loop_key || root_of(p) == loop_key) + to_erase.push_back(p); + } + for (const auto& p : to_erase) + before.erase(p); + } + add(expression_place_uses(*for_stmt->begin)); + add(expression_place_uses(*for_stmt->end)); + } else if (const auto* loop_stmt = dynamic_cast(&stmt)) { + LivePlaceSet header = live_after; + bool converged = false; + for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { + LivePlaceSet next = liveness_place_block(*loop_stmt->body, header, live_after, header); + if (next == header) { + converged = true; + break; + } + header = std::move(next); + } + if (!converged) + report_error(stmt.range, + "E0408", + "borrow liveness analysis did not converge; compilation stopped " + "conservatively"); + before = std::move(header); + } else if (dynamic_cast(&stmt)) { + before = break_live; + } else if (dynamic_cast(&stmt)) { + before = continue_live; + } else if (const auto* unsafe = dynamic_cast(&stmt)) { + before = liveness_place_block(*unsafe->body, live_after, break_live, continue_live); + } + + live_places_before_[&stmt] = before; + return before; +} + +void BorrowChecker::expire_dead_loans(const ast::Stmt& stmt, State& state) { + const auto live_it = live_before_.find(&stmt); + const auto live_places_it = live_places_before_.find(&stmt); + const LiveSet empty_roots; + const LivePlaceSet empty_places; + const LiveSet& live_roots = live_it == live_before_.end() ? empty_roots : live_it->second; + const LivePlaceSet& live_places = + live_places_it == live_places_before_.end() ? empty_places : live_places_it->second; + state.borrows.erase(std::remove_if(state.borrows.begin(), + state.borrows.end(), + [&](const Borrow& loan) { + if (loan.borrower == "") + return true; + // Two-phase reservations with `` borrower + // are statement-local; keep until activation. + if (loan.borrower.starts_with(" +#include + +namespace uinx { +using namespace borrow_detail; + +void BorrowChecker::set_reference_origins(const std::string& place, + std::vector origins, + State& state) { + for (auto it = state.ref_origins.begin(); it != state.ref_origins.end();) { + if (it->first == place || is_strict_child(place, it->first)) + it = state.ref_origins.erase(it); + else + ++it; + } + if (origins.empty()) + return; + + std::vector unique; + for (auto& origin : origins) { + const bool duplicate = std::any_of(unique.begin(), unique.end(), [&](const RefOrigin& x) { + return same_origin(x, origin); + }); + if (!duplicate) + unique.push_back(std::move(origin)); + } + state.ref_origins[place] = std::move(unique); +} + +void BorrowChecker::promote_reference_value(const std::string& borrower, + const std::vector& origins, + State& state) { + if (origins.empty()) + return; + + state.borrows.erase( + std::remove_if(state.borrows.begin(), + state.borrows.end(), + [&](const Borrow& loan) { + if (loan.borrower != "") + return false; + return std::any_of( + origins.begin(), origins.end(), [&](const RefOrigin& o) { + return overlaps(loan.place, o.place); + }); + }), + state.borrows.end()); + + for (const auto& origin : origins) + begin_borrow(origin.place, borrower, origin.mut, origin.range, state); +} + +void BorrowChecker::bind_reference_expression(const std::string& destination, + const ast::Expr& expression, + State& state) { + if (expression.kind == ast::ExprKind::StructLiteral) { + const auto& literal = static_cast(expression); + for (const auto& field : literal.fields) + bind_reference_expression(destination + "." + field.name, *field.value, state); + return; + } + + if (const auto source = place_of(expression)) { + std::vector>> mapped; + for (const auto& [stored_place, origins] : state.ref_origins) { + if (stored_place == *source || is_strict_child(*source, stored_place)) { + const std::string suffix = stored_place.substr(source->size()); + mapped.push_back({destination + suffix, origins}); + } + } + if (!mapped.empty()) { + const Type source_type = type_of_place(*source, state); + const bool transfer = !is_copy_type(source_type); + if (transfer) { + kill_borrowers_under(*source, state); + for (auto it = state.ref_origins.begin(); it != state.ref_origins.end();) { + if (it->first == *source || is_strict_child(*source, it->first)) + it = state.ref_origins.erase(it); + else + ++it; + } + } + for (auto& [target, origins] : mapped) { + promote_reference_value(target, origins, state); + set_reference_origins(target, std::move(origins), state); + } + return; + } + } + + auto origins = reference_origins(expression, state); + if (!origins.empty()) { + promote_reference_value(destination, origins, state); + set_reference_origins(destination, std::move(origins), state); + } +} + +void BorrowChecker::access_place(const std::string& place, + const SourceRange& range, + Access access, + State& state) { + if (access == Access::Move) { + const Type type = type_of_place(place, state); + if (is_copy_type(type)) + access = Access::Read; + } + + if (access == Access::Write) { + if (has_moved_ancestor(place, state)) { + report_error(range, + "E0400", + "cannot initialize field/place '" + display_place(place) + + "' because an enclosing value was moved"); + return; + } + } else if (unavailable(place, state)) { + report_error(range, "E0400", "use of moved value/place '" + display_place(place) + "'"); + return; + } + + auto is_active_mut = [](const Borrow& loan) { + if (loan.kind == BorrowKind::TwoPhaseReserved) + return false; // reservation acts as shared + return loan.mut; + }; + auto is_active_exclusive = [](const Borrow& loan) { + if (loan.kind == BorrowKind::TwoPhaseReserved) + return false; + return loan.kind == BorrowKind::Mut || loan.kind == BorrowKind::TwoPhaseActivated; + }; + + if (access == Access::Read) { + for (const auto& loan : state.borrows) { + if (places_conflict(place, loan.place) && is_active_mut(loan)) { + report_error(range, + "E0406", + "cannot read '" + display_place(place) + + "' while mutable borrow of '" + display_place(loan.place) + + "' is active"); + } + } + return; + } + + if (access == Access::BorrowShared) { + for (const auto& loan : state.borrows) { + if (places_conflict(place, loan.place) && is_active_mut(loan)) { + report_error(range, + "E0401", + "cannot immutably borrow '" + display_place(place) + + "' while mutable borrow of '" + display_place(loan.place) + + "' is active"); + } + } + return; + } + + if (access == Access::TwoPhaseReserve) { + // Reservation phase: only conflicts with active exclusive borrows. + for (const auto& loan : state.borrows) { + if (places_conflict(place, loan.place) && is_active_exclusive(loan)) { + report_error(range, + "E0402", + "cannot reserve mutably '" + display_place(place) + + "' while exclusive borrow of '" + display_place(loan.place) + + "' is active"); + } + } + return; + } + + if (access == Access::BorrowMut) { + for (const auto& loan : state.borrows) { + if (places_conflict(place, loan.place)) { + // A two-phase reservation of the same place by the same + // statement is upgraded at activation, not a conflict here. + // Direct overlapping live loans are still errors. + report_error(range, + "E0402", + "cannot mutably borrow '" + display_place(place) + + "' while borrow of '" + display_place(loan.place) + "' is active"); + } + } + return; + } + + if (access == Access::Write || access == Access::Move) { + for (const auto& loan : state.borrows) { + if (places_conflict(place, loan.place)) { + report_error(range, + "E0403", + "cannot " + std::string(access == Access::Move ? "move" : "write") + + " '" + display_place(place) + "' while it is borrowed"); + } + } + } + + if (access == Access::Move) + state.moved.insert(place); +} + +void BorrowChecker::begin_borrow(const std::string& place, + std::string borrower, + bool mut, + const SourceRange& range, + State& state) { + access_place(place, range, mut ? Access::BorrowMut : Access::BorrowShared, state); + Borrow loan; + loan.place = place; + loan.borrower = std::move(borrower); + loan.mut = mut; + loan.kind = mut ? BorrowKind::Mut : BorrowKind::Shared; + loan.origin = range; + loan.reservation = range; + loan.activated = mut; + if (std::none_of(state.borrows.begin(), state.borrows.end(), [&](const Borrow& existing) { + return same_borrow(existing, loan); + })) { + state.borrows.push_back(std::move(loan)); + } +} + +void BorrowChecker::begin_two_phase_reservation(const std::string& place, + std::string borrower, + const SourceRange& reservation, + State& state) { + access_place(place, reservation, Access::TwoPhaseReserve, state); + Borrow loan; + loan.place = place; + loan.borrower = std::move(borrower); + loan.mut = true; + loan.kind = BorrowKind::TwoPhaseReserved; + loan.origin = reservation; + loan.reservation = reservation; + loan.activated = false; + if (std::none_of(state.borrows.begin(), state.borrows.end(), [&](const Borrow& existing) { + return same_borrow(existing, loan); + })) { + state.borrows.push_back(std::move(loan)); + } +} + +void BorrowChecker::activate_two_phase(const std::string& borrower, + const SourceRange& activation, + State& state) { + for (auto& loan : state.borrows) { + if (loan.borrower == borrower && loan.kind == BorrowKind::TwoPhaseReserved) { + // Activation: now acts as exclusive. Check against live shared. + for (const auto& other : state.borrows) { + if (&other == &loan) + continue; + if (places_conflict(loan.place, other.place)) { + report_error(activation, + "E0402", + "cannot activate mutable borrow of '" + display_place(loan.place) + + "' while borrow of '" + display_place(other.place) + + "' is active"); + } + } + loan.kind = BorrowKind::TwoPhaseActivated; + loan.origin = activation; + loan.activated = true; + } + } +} + +bool BorrowChecker::is_two_phase_method_receiver(const ast::Expr& base, + const ast::CallExpr& call) const { + if (call.callee->kind != ast::ExprKind::Member) + return false; + const auto& member = static_cast(*call.callee); + Type receiver{}; + if (const auto type = model_.expr_types.find(&base); type != model_.expr_types.end()) + receiver = type->second; + Type owner = receiver; + if (owner.kind == TypeKind::Ref && owner.pointee) + owner = *owner.pointee; + const auto method = model_.functions.find(owner.name + "::" + member.member); + if (method == model_.functions.end() || method->second.params.empty()) + return false; + const Type& self_type = method->second.params.front(); + return self_type.kind == TypeKind::Ref && self_type.mut; +} + +void BorrowChecker::inspect_call_with_two_phase(const ast::CallExpr& call, State& state) { + // Two-phase protocol for `receiver.mut_method(args...)`: + // reserve receiver (shared-like), inspect args, then activate. + const auto& member = static_cast(*call.callee); + const auto receiver_place = place_of(*member.base); + const std::string reservation_id = ""; + bool reserved = false; + if (receiver_place) { + begin_two_phase_reservation(*receiver_place, reservation_id, call.range, state); + reserved = true; + } else { + inspect_expr(*member.base, state, Access::BorrowShared); + } + // Args are inspected while the receiver reservation is shared-like, so + // `v.push(v.len())` is accepted: `v.len()` shared-borrows while reserved. + for (const auto& arg : call.args) { + const auto type = model_.expr_types.find(arg.get()); + Access arg_access = Access::Move; + if (type != model_.expr_types.end() && type->second.kind == TypeKind::Ref && + !type->second.mut) { + arg_access = Access::Read; + } + inspect_expr(*arg, state, arg_access); + } + if (reserved) { + activate_two_phase(reservation_id, call.range, state); + // Call-duration exclusive borrow ends with the call (unless the + // return value carries provenance, handled via reference_origins). + kill_borrower(reservation_id, state); + } + // Receiver provenance: method may return borrow of receiver. + // Keep existing reference_origins handling via caller. +} +} // namespace uinx diff --git a/src/borrow_places.cpp b/src/borrow_places.cpp new file mode 100644 index 0000000..a763ee0 --- /dev/null +++ b/src/borrow_places.cpp @@ -0,0 +1,426 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "borrow_internal.hpp" +#include "uinx/borrow.hpp" + +#include +#include + +namespace uinx { +using namespace borrow_detail; + +void BorrowChecker::report_error(const SourceRange& range, std::string code, std::string message) { + std::ostringstream key; + key << code << '@' << range.begin.line << ':' << range.begin.column << '-' << range.end.line + << ':' << range.end.column << ':' << message; + if (emitted_errors_.insert(key.str()).second) + diags_.error(range, std::move(code), std::move(message)); +} + +bool BorrowChecker::overlaps(std::string_view a, std::string_view b) const { + return places_conflict(a, b); +} + +bool BorrowChecker::places_disjoint(std::string_view a, std::string_view b) const { + // Conservative external provenance always conflicts. + if (a.starts_with(" parent/child overlap. + return false; +} + +bool BorrowChecker::places_conflict(std::string_view a, std::string_view b) const { + return !places_disjoint(a, b); +} + +bool BorrowChecker::loan_live_at(const Borrow& loan, + const LiveSet& live_roots, + const LivePlaceSet& live_places) const { + if (loan.borrower == "") + return true; + const std::string borrower_root = root_of(loan.borrower); + if (model_.globals.contains(borrower_root)) + return true; + // Polonius-style: a loan is live only if its borrower may be used later. + // Check both root liveness (NLL) and precise place liveness. If either + // indicates a future use, the loan stays live. + if (live_roots.contains(borrower_root)) + return true; + if (live_places.contains(loan.borrower)) + return true; + // Subset edges: if any live place overlaps the borrower (e.g. a field of + // the borrower is live), the loan stays live. + for (const auto& live : live_places) { + if (places_conflict(live, loan.borrower)) + return true; + } + return false; +} + +bool BorrowChecker::same_origin(const RefOrigin& a, const RefOrigin& b) const { + return a.place == b.place && a.mut == b.mut && a.range.begin.line == b.range.begin.line && + a.range.begin.column == b.range.begin.column; +} + +bool BorrowChecker::same_borrow(const Borrow& a, const Borrow& b) const { + return a.place == b.place && a.borrower == b.borrower && a.mut == b.mut && a.kind == b.kind && + a.origin.begin.line == b.origin.begin.line && + a.origin.begin.column == b.origin.begin.column; +} + +std::optional BorrowChecker::place_of(const ast::Expr& expr) const { + if (expr.kind == ast::ExprKind::Name) { + const auto& name = static_cast(expr).name; + const auto resolved = model_.hir.expr_resolution.find(&expr); + if (resolved != model_.hir.expr_resolution.end() && + resolved->second < model_.hir.symbols.size()) { + const auto& symbol = model_.hir.symbols[resolved->second]; + if (symbol.kind == hir::SymbolKind::Local || symbol.kind == hir::SymbolKind::Parameter) + return symbol_key(name, resolved->second); + } + return name; + } + if (expr.kind == ast::ExprKind::Member) { + const auto& member = static_cast(expr); + auto base = place_of(*member.base); + if (base) + return *base + "." + member.member; + } + if (expr.kind == ast::ExprKind::Index) { + const auto& index = static_cast(expr); + auto base = place_of(*index.base); + if (base) + return *base + ".[*]"; + } + return std::nullopt; +} + +std::string BorrowChecker::binding_key(const ast::LetStmt& binding) const { + const auto found = model_.hir.bindings.find(&binding); + return found == model_.hir.bindings.end() ? binding.name + : symbol_key(binding.name, found->second); +} + +std::string BorrowChecker::parameter_key(const ast::Param& parameter) const { + const auto found = model_.hir.params.find(¶meter); + return found == model_.hir.params.end() ? parameter.name + : symbol_key(parameter.name, found->second); +} + +std::string BorrowChecker::for_binding_key(const ast::ForStmt& loop) const { + const auto found = model_.hir.for_bindings.find(&loop); + return found == model_.hir.for_bindings.end() ? loop.name + : symbol_key(loop.name, found->second); +} + +std::vector BorrowChecker::reference_origins(const ast::Expr& expr, + const State& state) const { + std::vector result; + auto append = [&](std::vector more) { + for (auto& origin : more) { + const bool duplicate = + std::any_of(result.begin(), result.end(), [&](const RefOrigin& x) { + return same_origin(x, origin); + }); + if (!duplicate) + result.push_back(std::move(origin)); + } + }; + + if (expr.kind == ast::ExprKind::Borrow) { + const auto& borrow = static_cast(expr); + if (auto place = place_of(*borrow.target)) { + result.push_back({*place, expr.range, borrow.mut}); + return result; + } + result = reference_origins(*borrow.target, state); + for (auto& origin : result) + origin.mut = borrow.mut; + return result; + } + + if (auto place = place_of(expr)) { + for (const auto& [stored_place, origins] : state.ref_origins) { + if (*place == stored_place || is_strict_child(*place, stored_place) || + is_strict_child(stored_place, *place)) { + append(origins); + } + } + if (!result.empty()) + return result; + } + + switch (expr.kind) { + case ast::ExprKind::StructLiteral: + for (const auto& field : static_cast(expr).fields) + append(reference_origins(*field.value, state)); + break; + case ast::ExprKind::Unary: + append(reference_origins(*static_cast(expr).operand, state)); + break; + case ast::ExprKind::Call: { + bool result_can_contain_reference = false; + if (const auto type = model_.expr_types.find(&expr); type != model_.expr_types.end()) { + std::unordered_set visiting; + result_can_contain_reference = contains_reference(type->second, visiting); + } + if (result_can_contain_reference) { + const auto& call = static_cast(expr); + if (call.callee->kind == ast::ExprKind::Member) { + const auto& member = static_cast(*call.callee); + auto receiver_origins = reference_origins(*member.base, state); + if (!receiver_origins.empty()) { + append(std::move(receiver_origins)); + } else if (const auto receiver_place = place_of(*member.base)) { + Type receiver_type{}; + if (const auto found = model_.expr_types.find(member.base.get()); + found != model_.expr_types.end()) + receiver_type = found->second; + + Type owner = receiver_type; + if (owner.kind == TypeKind::Ref && owner.pointee) + owner = *owner.pointee; + const auto method = + model_.functions.find(owner.name + "::" + member.member); + if (method != model_.functions.end() && !method->second.params.empty() && + method->second.params.front().kind == TypeKind::Ref) { + if (receiver_type.kind == TypeKind::Ref) { + // A reference parameter can point outside the current frame. Keep + // that provenance external inside this function; the caller maps + // it back to its concrete argument when the call itself is bound. + result.push_back({"", + expr.range, + method->second.params.front().mut}); + } else { + result.push_back({*receiver_place, + expr.range, + method->second.params.front().mut}); + } + } + } + } + for (const auto& arg : call.args) + append(reference_origins(*arg, state)); + } + break; + } + case ast::ExprKind::Member: + append(reference_origins(*static_cast(expr).base, state)); + break; + case ast::ExprKind::Index: + append(reference_origins(*static_cast(expr).base, state)); + break; + case ast::ExprKind::Cast: + append(reference_origins(*static_cast(expr).value, state)); + break; + case ast::ExprKind::Await: + append(reference_origins(*static_cast(expr).value, state)); + break; + default: + break; + } + return result; +} + +bool BorrowChecker::contains_reference(const Type& type, + std::unordered_set& visiting) const { + if (type.kind == TypeKind::Ref) + return true; + if (type.kind == TypeKind::Generic) + return true; + if (type.kind != TypeKind::Named) + return false; + if (!visiting.insert(type.name).second) + return false; + + const auto structure = model_.structs.find(type.name); + if (structure == model_.structs.end()) { + visiting.erase(type.name); + return false; + } + + std::unordered_map substitutions; + for (std::size_t i = 0; i < structure->second.generic_names.size() && i < type.args.size(); + ++i) { + substitutions[structure->second.generic_names[i]] = type.args[i]; + } + + for (const auto& [name, field] : structure->second.fields) { + (void)name; + if (contains_reference(substitute_type(field, substitutions), visiting)) { + visiting.erase(type.name); + return true; + } + } + visiting.erase(type.name); + return false; +} + +Type BorrowChecker::type_of_place(std::string_view place, const State& state) const { + const std::string root = root_of(place); + Type type; + bool found = false; + for (auto it = state.scopes.rbegin(); it != state.scopes.rend(); ++it) { + const auto binding = it->find(root); + if (binding != it->end()) { + type = binding->second; + found = true; + break; + } + } + if (!found) { + const auto global = model_.globals.find(root); + if (global != model_.globals.end()) { + type = global->second.type; + found = true; + } + } + if (!found) + return {}; + + std::size_t pos = root.size(); + while (pos < place.size() && place[pos] == '.') { + ++pos; + const auto next = place.find('.', pos); + const std::string field( + place.substr(pos, next == std::string_view::npos ? place.size() - pos : next - pos)); + if (type.kind == TypeKind::Ref && type.pointee) + type = *type.pointee; + if (field == "[*]") { + if (type.kind == TypeKind::Named && (type.name == "Slice" || type.name == "SliceMut") && + !type.args.empty()) { + type = type.args[0]; + } else { + return {}; + } + } else { + const auto structure = model_.structs.find(type.name); + if (structure == model_.structs.end()) + return {}; + const auto member = structure->second.fields.find(field); + if (member == structure->second.fields.end()) + return {}; + std::unordered_map substitutions; + for (std::size_t i = 0; + i < structure->second.generic_names.size() && i < type.args.size(); + ++i) { + substitutions[structure->second.generic_names[i]] = type.args[i]; + } + type = substitute_type(member->second, substitutions); + } + if (next == std::string_view::npos) + break; + pos = next; + } + return type; +} + +bool BorrowChecker::unavailable(std::string_view place, const State& state) const { + return std::any_of(state.moved.begin(), state.moved.end(), [&](const std::string& moved) { + return overlaps(moved, place); + }); +} + +bool BorrowChecker::has_moved_ancestor(std::string_view place, const State& state) const { + return std::any_of(state.moved.begin(), state.moved.end(), [&](const std::string& moved) { + return moved != place && is_strict_child(moved, place); + }); +} + +bool BorrowChecker::is_stack_owned(std::string_view place, const State& state) const { + const std::string root = root_of(place); + for (const auto& scope : state.scopes) { + const auto binding = scope.find(root); + if (binding != scope.end()) { + // `ref T`/`mutref T` parameters and locals are stack bindings, but a field/index + // reached through them belongs to the referent, not to the reference slot itself. + // Borrowing the reference slot (`borrow r`) still correctly counts as stack-owned. + if (binding->second.kind == TypeKind::Ref && is_strict_child(root, place)) + return false; + return true; + } + } + return false; +} + +bool BorrowChecker::is_copy_type(const Type& type) const { + if (type.kind == TypeKind::Generic && copy_generics_.contains(type.name)) + return true; + if (type.is_copy()) + return true; + TraitSolver solver(model_); + return solver.satisfies(type, "Copy"); +} + +bool BorrowChecker::states_equivalent(const State& a, const State& b) const { + if (a.reachable != b.reachable || a.moved != b.moved || a.scopes.size() != b.scopes.size() || + a.borrows.size() != b.borrows.size() || a.ref_origins.size() != b.ref_origins.size()) { + return false; + } + for (const auto& borrow : a.borrows) { + if (std::none_of(b.borrows.begin(), b.borrows.end(), [&](const Borrow& other) { + return same_borrow(borrow, other); + })) { + return false; + } + } + for (const auto& [place, origins] : a.ref_origins) { + const auto found = b.ref_origins.find(place); + if (found == b.ref_origins.end() || found->second.size() != origins.size()) + return false; + for (const auto& origin : origins) { + if (std::none_of(found->second.begin(), + found->second.end(), + [&](const RefOrigin& other) { return same_origin(origin, other); })) { + return false; + } + } + } + return true; +} + +void BorrowChecker::mark_initialized(std::string_view place, State& state) { + for (auto it = state.moved.begin(); it != state.moved.end();) { + if (*it == place || is_strict_child(place, *it)) + it = state.moved.erase(it); + else + ++it; + } +} + +void BorrowChecker::kill_borrower(std::string_view borrower, State& state) { + state.borrows.erase( + std::remove_if(state.borrows.begin(), + state.borrows.end(), + [&](const Borrow& loan) { return loan.borrower == borrower; }), + state.borrows.end()); +} + +void BorrowChecker::kill_borrowers_under(std::string_view borrower, State& state) { + state.borrows.erase(std::remove_if(state.borrows.begin(), + state.borrows.end(), + [&](const Borrow& loan) { + return loan.borrower == borrower || + is_strict_child(borrower, loan.borrower); + }), + state.borrows.end()); +} +} // namespace uinx diff --git a/src/codegen.cpp b/src/codegen.cpp index 99a5544..65c997f 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -2,10 +2,10 @@ // Copyright (c) 2026 ViudiraTech // By JiTianYu391 -// Uinx Language - #include "uinx/codegen.hpp" +#include "codegen_internal.hpp" + #include #include #include @@ -14,351 +14,7 @@ #include namespace uinx { -namespace { - -bool unsigned_integer(TypeKind kind) { - return kind == TypeKind::U8 || kind == TypeKind::U16 || kind == TypeKind::U32 || - kind == TypeKind::U64 || kind == TypeKind::U128 || kind == TypeKind::Usize; -} - -int integer_bits(const Type& type) { - switch (type.kind) { - case TypeKind::I8: - case TypeKind::U8: - return 8; - case TypeKind::I16: - case TypeKind::U16: - return 16; - case TypeKind::I32: - case TypeKind::U32: - case TypeKind::Char: - return 32; - case TypeKind::I128: - case TypeKind::U128: - return 128; - default: - return 64; - } -} - -std::string llvm_integer_constant(std::string_view literal) { - bool negative = false; - std::size_t offset = 0; - if (!literal.empty() && literal.front() == '-') { - negative = true; - offset = 1; - } - - unsigned base = 10; - if (literal.size() >= offset + 2 && literal[offset] == '0') { - switch (literal[offset + 1]) { - case 'x': - case 'X': - base = 16; - offset += 2; - break; - case 'b': - case 'B': - base = 2; - offset += 2; - break; - case 'o': - case 'O': - base = 8; - offset += 2; - break; - default: - break; - } - } - - // Store the magnitude in base 1,000,000,000 so this also handles u128 - // without narrowing through a host integer type. - std::vector digits{0}; - bool has_digit = false; - for (std::size_t i = offset; i < literal.size(); ++i) { - const char c = literal[i]; - if (c == '_') - continue; - unsigned value = 0; - if (c >= '0' && c <= '9') - value = static_cast(c - '0'); - else if (c >= 'a' && c <= 'f') - value = static_cast(c - 'a') + 10; - else if (c >= 'A' && c <= 'F') - value = static_cast(c - 'A') + 10; - else - return std::string(literal); - if (value >= base) - return std::string(literal); - - has_digit = true; - std::uint64_t carry = value; - for (auto& digit : digits) { - const std::uint64_t next = static_cast(digit) * base + carry; - digit = static_cast(next % 1000000000U); - carry = next / 1000000000U; - } - if (carry != 0) - digits.push_back(static_cast(carry)); - } - if (!has_digit) - return std::string(literal); - - while (digits.size() > 1 && digits.back() == 0) - digits.pop_back(); - if (digits.size() == 1 && digits.front() == 0) - return "0"; - - std::string result = std::to_string(digits.back()); - for (std::size_t i = digits.size() - 1; i > 0; --i) { - const std::string chunk = std::to_string(digits[i - 1]); - result.append(9 - chunk.size(), '0'); - result += chunk; - } - if (negative) - result.insert(result.begin(), '-'); - return result; -} - -std::string arithmetic_opcode(const Type& type, std::string_view op) { - if (op == "+") - return type.is_float() ? "fadd" : "add"; - if (op == "-") - return type.is_float() ? "fsub" : "sub"; - if (op == "*") - return type.is_float() ? "fmul" : "mul"; - if (op == "/") { - if (type.is_float()) - return "fdiv"; - return unsigned_integer(type.kind) ? "udiv" : "sdiv"; - } - if (op == "%") - return unsigned_integer(type.kind) ? "urem" : "srem"; - if (op == "&&" || op == "&") - return "and"; - if (op == "||" || op == "|") - return "or"; - if (op == "^") - return "xor"; - if (op == "<<") - return "shl"; - if (op == ">>") - return unsigned_integer(type.kind) ? "lshr" : "ashr"; - return "add"; -} - -unsigned type_alignment(const Type& type) { - switch (type.kind) { - case TypeKind::Bool: - case TypeKind::I8: - case TypeKind::U8: - return 1; - case TypeKind::I16: - case TypeKind::U16: - return 2; - case TypeKind::I32: - case TypeKind::U32: - case TypeKind::Char: - case TypeKind::F32: - return 4; - case TypeKind::I128: - case TypeKind::U128: - return 16; - default: - return 8; - } -} - -std::string atomic_rmw_opcode(std::string_view op) { - if (op == "+") - return "add"; - if (op == "-") - return "sub"; - if (op == "&") - return "and"; - if (op == "|") - return "or"; - if (op == "^") - return "xor"; - return "xchg"; -} - -std::optional atomic_order_from_abi(std::string_view value) { - if (value == "0") - return "monotonic"; - if (value == "1") - return "acquire"; - if (value == "2") - return "release"; - if (value == "3") - return "acq_rel"; - if (value == "4") - return "seq_cst"; - return std::nullopt; -} - -std::string cmpxchg_failure_order(std::string order) { - if (order == "release") - return "monotonic"; - if (order == "acq_rel") - return "acquire"; - return order; -} - -// Industrial C++20 ordering validation (Rust + C++20 + LKMM fusion). -// load: relaxed/acquire/seq_cst; store: relaxed/release/seq_cst; -// rmw/exchange: all five; cmpxchg failure: never release/acq_rel. -bool valid_atomic_load_order(std::string_view order) { - return order == "monotonic" || order == "acquire" || order == "seq_cst"; -} - -bool valid_atomic_store_order(std::string_view order) { - return order == "monotonic" || order == "release" || order == "seq_cst"; -} - -bool valid_atomic_rmw_order(std::string_view order) { - return order == "monotonic" || order == "acquire" || order == "release" || order == "acq_rel" || - order == "seq_cst"; -} - -int order_strength(std::string_view order) { - if (order == "monotonic") - return 0; - if (order == "acquire" || order == "release") - return 1; - if (order == "acq_rel") - return 2; - if (order == "seq_cst") - return 3; - return -1; -} - -std::string downgrade_cmpxchg_failure(std::string success, std::string failure) { - failure = cmpxchg_failure_order(std::move(failure)); - // Failure ordering must not exceed success ordering. - if (order_strength(failure) > order_strength(success)) { - if (success == "monotonic" || success == "release") - return "monotonic"; - return "acquire"; - } - return failure; -} - -struct AtomicIntrinsic { - std::string llvm_type; - unsigned align; - std::string rmw_op; // add/sub/and/or/xor/xchg/cmpxchg/load/store -}; - -std::optional parse_atomic_intrinsic(std::string_view name) { - // uinx_atomic_{load,store,fetch_add,fetch_sub,fetch_and,fetch_or,fetch_xor, - // exchange,compare_exchange}_{u8,u16,u32,u64} - static constexpr std::string_view prefix = "uinx_atomic_"; - if (!name.starts_with(prefix)) - return std::nullopt; - std::string_view rest = name.substr(prefix.size()); - std::string op; - for (std::string_view candidate : {"compare_exchange", - "fetch_add", - "fetch_sub", - "fetch_and", - "fetch_or", - "fetch_xor", - "exchange", - "load", - "store"}) { - if (rest.starts_with(candidate)) { - op = std::string(candidate); - rest = rest.substr(candidate.size()); - break; - } - } - if (op.empty() || !rest.starts_with("_")) - return std::nullopt; - std::string_view width = rest.substr(1); - std::string llvm_type; - unsigned align = 1; - if (width == "u8") { - llvm_type = "i8"; - align = 1; - } else if (width == "u16") { - llvm_type = "i16"; - align = 2; - } else if (width == "u32") { - llvm_type = "i32"; - align = 4; - } else if (width == "u64") { - llvm_type = "i64"; - align = 8; - } else { - return std::nullopt; - } - std::string rmw; - if (op == "load") - rmw = "load"; - else if (op == "store") - rmw = "store"; - else if (op == "fetch_add") - rmw = "add"; - else if (op == "fetch_sub") - rmw = "sub"; - else if (op == "fetch_and") - rmw = "and"; - else if (op == "fetch_or") - rmw = "or"; - else if (op == "fetch_xor") - rmw = "xor"; - else if (op == "exchange") - rmw = "xchg"; - else if (op == "compare_exchange") - rmw = "cmpxchg"; - return AtomicIntrinsic{llvm_type, align, rmw}; -} - -// Number of arguments encoded in a uinx_syscallN intrinsic name, or -// std::nullopt when the symbol is an ordinary runtime call. -std::optional syscall_arity_from_name(std::string_view name) { - static constexpr std::string_view prefix = "uinx_syscall"; - if (!name.starts_with(prefix)) - return std::nullopt; - const std::string_view suffix = name.substr(prefix.size()); - if (suffix.empty() || suffix.size() > 1) - return std::nullopt; - if (suffix.front() < '0' || suffix.front() > '6') - return std::nullopt; - return suffix.front() - '0'; -} - -std::string compare_predicate(const Type& type, std::string_view op) { - if (type.is_float()) { - if (op == "==") - return "oeq"; - if (op == "!=") - return "une"; - if (op == "<") - return "olt"; - if (op == "<=") - return "ole"; - if (op == ">") - return "ogt"; - return "oge"; - } - const bool u = unsigned_integer(type.kind); - if (op == "==") - return "eq"; - if (op == "!=") - return "ne"; - if (op == "<") - return u ? "ult" : "slt"; - if (op == "<=") - return u ? "ule" : "sle"; - if (op == ">") - return u ? "ugt" : "sgt"; - return u ? "uge" : "sge"; -} - -} // namespace +using namespace codegen_detail; TargetInfo TargetInfo::from_triple(std::string triple) { TargetInfo target; @@ -473,1271 +129,6 @@ std::string LLVMCodegen::escape_asm(std::string_view value) const { return out; } -bool LLVMCodegen::validate_constraint(std::string_view constraint, const SourceRange& range) const { - std::string raw(constraint); - while (!raw.empty() && std::string("=+&~").find(raw.front()) != std::string::npos) - raw.erase(raw.begin()); - if (raw == "memory" || raw == "cc" || raw == "r" || raw == "m" || raw == "i" || raw == "g") - return true; - if (raw.size() >= 2 && raw.front() == '{' && raw.back() == '}') - raw = raw.substr(1, raw.size() - 2); - - static const std::unordered_set x86 = { - "rax", "rbx", "rcx", "rdx", "rsi", "rdi", "rbp", "rsp", "r8", - "r9", "r10", "r11", "r12", "r13", "r14", "r15", "eax", "ebx", - "ecx", "edx", "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", - "xmm7", "xmm8", "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15"}; - auto numbered = [&](char prefix, int max) { - if (raw.size() < 2 || raw.front() != prefix) - return false; - try { - const int n = std::stoi(raw.substr(1)); - return n >= 0 && n <= max; - } catch (...) { - return false; - } - }; - - bool valid = false; - if (target_.arch == "x86_64") { - valid = x86.contains(raw); - } else if (target_.arch == "aarch64") { - valid = numbered('x', 30) || numbered('w', 30) || numbered('v', 31) || raw == "sp"; - } else if (target_.arch == "riscv64") { - valid = numbered('x', 31) || raw == "ra" || raw == "sp" || raw == "gp" || raw == "tp" || - raw == "a0" || raw == "a1" || raw == "a2" || raw == "a3" || raw == "a4" || - raw == "a5" || raw == "a6" || raw == "a7" || raw == "s0" || raw == "s1" || - raw == "t0" || raw == "t1" || raw == "t2"; - } else { - valid = true; - } - if (!valid) { - diags_.error(range, - "E0600", - "asm constraint/register '" + std::string(constraint) + - "' is not valid for target architecture '" + target_.arch + "'"); - } - return valid; -} - -void LLVMCodegen::emit_inline_asm(std::ostream& os, - const mir::Instruction& in, - std::unordered_map& values, - std::unordered_map& value_types, - std::size_t& counter) const { - std::vector outputs; - std::vector inputs; - std::vector clobbers; - for (const auto& operand : in.asm_operands) { - validate_constraint(operand.constraint, in.range); - if (operand.kind == ast::AsmOperand::Kind::Out || - operand.kind == ast::AsmOperand::Kind::InOut) - outputs.push_back(&operand); - else if (operand.kind == ast::AsmOperand::Kind::In) - inputs.push_back(&operand); - else - clobbers.push_back(&operand); - } - - std::vector constraints; - std::vector> call_args; - for (const auto* output : outputs) { - std::string c = output->constraint; - while (!c.empty() && c.front() == '+') - c.erase(c.begin()); - if (c.empty() || c.front() != '=') - c = "=" + c; - constraints.push_back(std::move(c)); - } - for (std::size_t index = 0; index < outputs.size(); ++index) { - const auto* output = outputs[index]; - if (output->kind != ast::AsmOperand::Kind::InOut) - continue; - std::string initial = "undef"; - if (!output->out_slot.empty()) { - initial = "%asm.inout." + std::to_string(counter) + "." + std::to_string(index); - os << " " << initial << " = load " << llvm_type(output->type) << ", ptr " - << output->out_slot << "\n"; - } else if (!output->value.empty()) { - const auto it = values.find(output->value); - initial = it == values.end() ? output->value : it->second; - } - constraints.push_back(std::to_string(index)); - call_args.push_back({output->type, initial}); - } - for (const auto* input : inputs) { - constraints.push_back(input->constraint); - const auto it = values.find(input->value); - call_args.push_back({input->type, it == values.end() ? input->value : it->second}); - } - for (const auto* clobber : clobbers) { - if (clobber->constraint == "memory") - constraints.push_back("~{memory}"); - else if (clobber->constraint == "cc") - constraints.push_back("~{cc}"); - else { - std::string reg = clobber->constraint; - if (reg.front() == '{' && reg.back() == '}') - constraints.push_back("~" + reg); - else - constraints.push_back("~{" + reg + "}"); - } - } - - std::string return_type = "void"; - if (outputs.size() == 1) - return_type = llvm_type(outputs.front()->type); - else if (outputs.size() > 1) { - return_type = "{ "; - for (std::size_t i = 0; i < outputs.size(); ++i) { - if (i) - return_type += ", "; - return_type += llvm_type(outputs[i]->type); - } - return_type += " }"; - } - - std::string constraint_text; - for (std::size_t i = 0; i < constraints.size(); ++i) { - if (i) - constraint_text += ','; - constraint_text += constraints[i]; - } - - const std::string call_result = outputs.empty() ? "" : "%asm.result." + std::to_string(counter); - os << " "; - if (!call_result.empty()) - os << call_result << " = "; - os << "call " << return_type << " asm "; - if (in.flag) - os << "sideeffect "; - os << '"' << escape_asm(in.text) << "\", \"" << escape_asm(constraint_text) << "\"("; - for (std::size_t i = 0; i < call_args.size(); ++i) { - if (i) - os << ", "; - os << llvm_type(call_args[i].first) << ' ' << call_args[i].second; - } - os << ")\n"; - - for (std::size_t i = 0; i < outputs.size(); ++i) { - const auto* output = outputs[i]; - if (output->out_slot.empty()) - continue; - std::string value = call_result; - if (outputs.size() > 1) { - value = "%asm.out." + std::to_string(counter) + "." + std::to_string(i); - os << " " << value << " = extractvalue " << return_type << ' ' << call_result << ", " - << i << "\n"; - } - os << " store " << llvm_type(output->type) << ' ' << value << ", ptr " << output->out_slot - << "\n"; - value_types[output->out_slot] = output->type; - } - ++counter; -} - -// Lowers the compiler-recognized `uinx_syscall0..6` ABI family into a raw -// kernel trap for the current target. Argument 0 is the syscall number, the -// remaining arguments map to the architecture's syscall argument registers. -// Returns false when the target has no known trap sequence so the caller can -// fall back to an ordinary call. -bool LLVMCodegen::emit_syscall(std::ostream& os, - const mir::Instruction& in, - std::unordered_map& values, - std::unordered_map& value_types, - std::size_t& counter) const { - const int arity = static_cast(in.args.size()); - if (arity < 1 || arity > 7) - return false; - - auto value_of = [&](const std::string& value) -> std::string { - const auto it = values.find(value); - return it == values.end() ? value : it->second; - }; - - // Zero-extend each argument into an i64 so register constraints are - // satisfied regardless of the source integer width. - std::vector args; - args.reserve(static_cast(arity)); - for (const auto& arg : in.args) { - const std::string raw = value_of(arg); - const auto type_it = value_types.find(arg); - const Type type = type_it == value_types.end() ? Type::builtin("i64") : type_it->second; - if (type.str() == "i64" || type.str() == "u64" || type.str() == "usize" || - type.str() == "isize") { - args.push_back(raw); - } else { - args.push_back("%syscall.arg." + std::to_string(counter) + "." + - std::to_string(args.size())); - os << " " << args.back() << " = zext " << llvm_type(type) << ' ' << raw << " to i64\n"; - } - } - - std::string constraint_text; - std::string asm_text; - if (target_.arch == "x86_64") { - // rax = number, rdi/rsi/rdx/r10/r8/r9 = args 1..6. Only the registers - // actually consumed are declared; the kernel additionally clobbers rcx - // (return RIP) and r11 (RFLAGS). - static const char* regs[7] = {"rax", "rdi", "rsi", "rdx", "r10", "r8", "r9"}; - std::vector constraints{"={rax}"}; - for (int i = 0; i < arity; ++i) - constraints.push_back(std::string("{") + regs[i] + "}"); - constraints.push_back("~{rcx}"); - constraints.push_back("~{r11}"); - constraints.push_back("~{memory}"); - constraints.push_back("~{cc}"); - for (std::size_t i = 0; i < constraints.size(); ++i) { - if (i) - constraint_text += ','; - constraint_text += constraints[i]; - } - asm_text = "syscall"; - } else if (target_.arch == "aarch64") { - // x8 = number, x0..x5 = args 1..6. The kernel may clobber x16/x17. - static const char* regs[7] = {"x8", "x0", "x1", "x2", "x3", "x4", "x5"}; - std::vector constraints{"={x0}"}; - for (int i = 0; i < arity; ++i) - constraints.push_back(std::string("{") + regs[i] + "}"); - constraints.push_back("~{x16}"); - constraints.push_back("~{x17}"); - constraints.push_back("~{memory}"); - constraints.push_back("~{cc}"); - for (std::size_t i = 0; i < constraints.size(); ++i) { - if (i) - constraint_text += ','; - constraint_text += constraints[i]; - } - asm_text = "svc #0"; - } else if (target_.arch == "riscv64") { - // a7 = number, a0..a5 = args 1..6. The kernel may clobber a6. - static const char* regs[7] = {"a7", "a0", "a1", "a2", "a3", "a4", "a5"}; - std::vector constraints{"={a0}"}; - for (int i = 0; i < arity; ++i) - constraints.push_back(std::string("{") + regs[i] + "}"); - constraints.push_back("~{a6}"); - constraints.push_back("~{memory}"); - constraints.push_back("~{cc}"); - for (std::size_t i = 0; i < constraints.size(); ++i) { - if (i) - constraint_text += ','; - constraint_text += constraints[i]; - } - asm_text = "ecall"; - } else { - diags_.error(in.range, - "E0601", - "no syscall trap sequence is defined for target architecture '" + - target_.arch + "'"); - return false; - } - - const std::string call_result = "%syscall.result." + std::to_string(counter); - os << " " << call_result << " = call i64 asm sideeffect \"" << asm_text << "\", \"" - << constraint_text << "\"("; - for (std::size_t i = 0; i < args.size(); ++i) { - if (i) - os << ", "; - os << "i64 " << args[i]; - } - os << ")\n"; - - if (!in.result.empty()) { - // The trap returns i64; narrow to the declared result type when needed. - const std::string result_type = llvm_type(in.type); - if (result_type == "i64") { - values[in.result] = call_result; - } else { - const std::string truncated = "%syscall.trunc." + std::to_string(counter); - os << " " << truncated << " = trunc i64 " << call_result << " to " << result_type - << "\n"; - values[in.result] = truncated; - } - value_types[in.result] = in.type; - } - ++counter; - return true; -} - -void LLVMCodegen::emit_async_function( - std::ostream& os, - const mir::Function& fn, - const std::unordered_map& functions) const { - struct FrameField { - Type type; - std::size_t index{}; - bool address{false}; - }; - - std::unordered_map fields; - std::vector> ordered; - std::size_t next_field = 1; - auto add_field = [&](const std::string& key, Type type, bool address) { - if (key.empty() || fields.contains(key)) - return; - FrameField field{std::move(type), next_field++, address}; - fields.emplace(key, field); - ordered.push_back({key, field}); - }; - - for (const auto& param : fn.params) - add_field("%arg." + param.name, param.type, false); - std::vector> awaits; - std::size_t await_count = 0; - for (const auto& block : fn.blocks) { - for (const auto& instruction : block.instructions) { - if (instruction.op == mir::Op::Await) - awaits.push_back({&instruction, ++await_count}); - if (instruction.result.empty()) - continue; - if (instruction.op == mir::Op::Alloca) - add_field(instruction.result, instruction.type, true); - else if (instruction.op == mir::Op::FieldAddr || instruction.op == mir::Op::IndexAddr) - add_field(instruction.result, Type::raw_ptr(instruction.type, true), false); - else - add_field(instruction.result, instruction.type, false); - } - } - - const std::string name = llvm_name(fn.name); - const std::string frame_type = "%uinx.async.frame." + name; - os << frame_type << " = type { i32"; - for (const auto& [key, field] : ordered) { - (void)key; - os << ", " << llvm_type(field.type); - } - os << " }\n"; - os << "@" << name << "__vtable = private constant %uinx.future.vtable { ptr @" << name - << "__poll, ptr @" << name << "__drop }\n"; - - os << "define %uinx.future @" << name << '('; - for (std::size_t i = 0; i < fn.params.size(); ++i) { - if (i) - os << ", "; - os << llvm_type(fn.params[i].type) << " %arg." << llvm_name(fn.params[i].name); - } - os << ") {\nentry:\n"; - os << " %frame.size.ptr = getelementptr " << frame_type << ", ptr null, i32 1\n"; - os << " %frame.size = ptrtoint ptr %frame.size.ptr to i64\n"; - os << " %frame = call ptr @malloc(i64 %frame.size)\n"; - os << " %oom = icmp eq ptr %frame, null\n"; - os << " br i1 %oom, label %async.oom, label %async.init\n"; - os << "async.oom:\n call void @abort()\n unreachable\n"; - os << "async.init:\n"; - os << " %state.addr = getelementptr inbounds " << frame_type << ", ptr %frame, i32 0, i32 0\n"; - os << " store i32 0, ptr %state.addr\n"; - for (const auto& param : fn.params) { - const auto& field = fields.at("%arg." + param.name); - os << " %param.addr." << field.index << " = getelementptr inbounds " << frame_type - << ", ptr %frame, i32 0, i32 " << field.index << "\n"; - os << " store " << llvm_type(param.type) << " %arg." << llvm_name(param.name) - << ", ptr %param.addr." << field.index << "\n"; - } - os << " %future.0 = insertvalue %uinx.future poison, ptr %frame, 0\n"; - os << " %future.1 = insertvalue %uinx.future %future.0, ptr @" << name << "__vtable, 1\n"; - os << " ret %uinx.future %future.1\n}\n\n"; - - os << "define i32 @" << name << "__poll(ptr %frame, ptr %output) {\nentry:\n"; - os << " %state.addr = getelementptr inbounds " << frame_type << ", ptr %frame, i32 0, i32 0\n"; - os << " %state = load i32, ptr %state.addr\n"; - const std::string first_label = fn.blocks.empty() ? "entry" : fn.blocks.front().label; - os << " switch i32 %state, label %async.invalid [ i32 0, label %mir." - << llvm_name(first_label); - for (const auto& [instruction, id] : awaits) { - (void)instruction; - os << " i32 " << id << ", label %async.resume." << id; - } - os << " i32 -1, label %async.completed ]\n"; - os << "async.invalid:\n ret i32 -22\n"; - os << "async.completed:\n ret i32 1\n"; - for (const auto& [instruction, id] : awaits) { - (void)instruction; - os << "async.resume." << id << ":\n br label %async.await." << id << ".poll\n"; - } - - std::size_t temp_counter = 0; - std::size_t asm_counter = 0; - std::size_t bounds_counter = 0; - std::size_t current_await = 0; - std::unordered_map value_types; - for (const auto& [key, field] : ordered) - value_types[key] = field.type; - - auto frame_addr = [&](const std::string& key) -> std::string { - const auto it = fields.find(key); - if (it == fields.end()) - return key; - const std::string temp = "%af.addr." + std::to_string(temp_counter++); - os << " " << temp << " = getelementptr inbounds " << frame_type - << ", ptr %frame, i32 0, i32 " << it->second.index << "\n"; - return temp; - }; - auto value_of = [&](const std::string& value) -> std::string { - const auto it = fields.find(value); - if (it == fields.end()) - return value; - const std::string address = frame_addr(value); - if (it->second.address) - return address; - const std::string temp = "%af.load." + std::to_string(temp_counter++); - os << " " << temp << " = load " << llvm_type(it->second.type) << ", ptr " << address - << "\n"; - return temp; - }; - auto store_result = [&](const mir::Instruction& instruction, const std::string& value) { - if (instruction.result.empty()) - return; - const auto it = fields.find(instruction.result); - if (it == fields.end() || it->second.address) - return; - const std::string address = frame_addr(instruction.result); - os << " store " << llvm_type(it->second.type) << ' ' << value << ", ptr " << address - << "\n"; - }; - auto mir_label = [&](const std::string& label) { return "mir." + llvm_name(label); }; - - for (const auto& block : fn.blocks) { - os << mir_label(block.label) << ":\n"; - for (const auto& instruction : block.instructions) { - switch (instruction.op) { - case mir::Op::Alloca: - break; - case mir::Op::ConstInt: - store_result(instruction, llvm_integer_constant(instruction.text)); - break; - case mir::Op::ConstFloat: - case mir::Op::ConstBool: - case mir::Op::ConstChar: - store_result(instruction, instruction.text); - break; - case mir::Op::ConstString: { - store_result(instruction, - "getelementptr inbounds (i8, ptr @" + llvm_name(instruction.text) + - ", i64 0)"); - break; - } - case mir::Op::StructInit: { - if (instruction.args.empty()) { - store_result(instruction, "zeroinitializer"); - break; - } - std::string aggregate = "poison"; - for (std::size_t index = 0; index < instruction.args.size(); ++index) { - const std::string arg = value_of(instruction.args[index]); - const Type arg_type = value_types.contains(instruction.args[index]) - ? value_types[instruction.args[index]] - : Type::builtin("i32"); - const std::string next = "%af.insert." + std::to_string(temp_counter++); - os << " " << next << " = insertvalue " << llvm_type(instruction.type) - << ' ' << aggregate << ", " << llvm_type(arg_type) << ' ' << arg << ", " - << index << "\n"; - aggregate = next; - } - store_result(instruction, aggregate); - break; - } - case mir::Op::Load: { - const std::string address = value_of(instruction.args[0]); - const std::string result = "%af.op." + std::to_string(temp_counter++); - os << " " << result << " = load "; - if (instruction.flag) - os << "atomic "; - os << llvm_type(instruction.type) << ", ptr " << address; - if (instruction.flag) - os << ' ' << instruction.text << ", align " - << type_alignment(instruction.type); - os << "\n"; - store_result(instruction, result); - break; - } - case mir::Op::Store: { - const std::string value = value_of(instruction.args[0]); - const std::string address = value_of(instruction.args[1]); - os << " store "; - if (instruction.flag) - os << "atomic "; - os << llvm_type(instruction.type) << ' ' << value << ", ptr " << address; - if (instruction.flag) - os << ' ' << instruction.text << ", align " - << type_alignment(instruction.type); - os << "\n"; - break; - } - case mir::Op::AtomicRmw: { - const std::string address = value_of(instruction.args[0]); - const std::string value = value_of(instruction.args[1]); - const std::string result = "%af.atomic." + std::to_string(temp_counter++); - os << " " << result << " = atomicrmw " << atomic_rmw_opcode(instruction.text) - << " ptr " << address << ", " << llvm_type(instruction.type) << ' ' << value - << ' ' << instruction.args[2] << ", align " - << type_alignment(instruction.type) << "\n"; - break; - } - case mir::Op::Fence: - os << " fence " << instruction.text << "\n"; - break; - case mir::Op::CompilerFence: - os << " fence syncscope(\"singlethread\") " << instruction.text << "\n"; - break; - case mir::Op::FieldAddr: { - const std::string base = value_of(instruction.args[0]); - const std::string result = "%af.field." + std::to_string(temp_counter++); - os << " " << result << " = getelementptr inbounds " - << llvm_type(instruction.auxiliary_type) << ", ptr " << base - << ", i32 0, i32 " << instruction.text << "\n"; - store_result(instruction, result); - break; - } - case mir::Op::IndexAddr: { - const std::string data = value_of(instruction.args[0]), - index = value_of(instruction.args[1]), - len = value_of(instruction.args[2]); - const int bits = integer_bits(instruction.auxiliary_type); - const bool is_unsigned = unsigned_integer(instruction.auxiliary_type.kind); - const std::string suffix = std::to_string(bounds_counter++); - std::string index64 = index, ok; - if (bits > 64) { - const std::string len_ext = "%af.bound.len." + suffix; - os << " " << len_ext << " = zext i64 " << len << " to " - << llvm_type(instruction.auxiliary_type) << "\n"; - const std::string lt = "%af.bound.lt." + suffix; - os << " " << lt << " = icmp ult " << llvm_type(instruction.auxiliary_type) - << " " << index << ", " << len_ext << "\n"; - ok = lt; - if (!is_unsigned) { - const std::string nonneg = "%af.bound.nonneg." + suffix; - os << " " << nonneg << " = icmp sge " - << llvm_type(instruction.auxiliary_type) << " " << index << ", 0\n"; - const std::string both = "%af.bound.ok." + suffix; - os << " " << both << " = and i1 " << nonneg << ", " << lt << "\n"; - ok = both; - } - index64 = "%af.bound.index." + suffix; - os << " " << index64 << " = trunc " - << llvm_type(instruction.auxiliary_type) << " " << index << " to i64\n"; - } else { - if (bits < 64) { - index64 = "%af.bound.index." + suffix; - os << " " << index64 << " = " << (is_unsigned ? "zext" : "sext") << " " - << llvm_type(instruction.auxiliary_type) << " " << index - << " to i64\n"; - } - ok = "%af.bound.ok." + suffix; - os << " " << ok << " = icmp ult i64 " << index64 << ", " << len << "\n"; - } - os << " br i1 " << ok << ", label %async.bounds.ok." << suffix - << ", label %async.bounds.fail." << suffix << "\n"; - os << "async.bounds.fail." << suffix - << ":\n call void @llvm.trap()\n unreachable\n"; - os << "async.bounds.ok." << suffix << ":\n"; - const std::string result = "%af.index." + suffix; - os << " " << result << " = getelementptr inbounds " - << llvm_type(instruction.type) << ", ptr " << data << ", i64 " << index64 - << "\n"; - store_result(instruction, result); - break; - } - case mir::Op::AddressOf: - store_result(instruction, value_of(instruction.args[0])); - break; - case mir::Op::Binary: { - const std::string lhs = value_of(instruction.args[0]); - const std::string result = "%af.bin." + std::to_string(temp_counter++); - if (instruction.type.kind == TypeKind::RawPtr && - (instruction.text == "+" || instruction.text == "-") && - instruction.args.size() == 2) { - const std::string rhs = value_of(instruction.args[1]); - const Type index_type = value_types.contains(instruction.args[1]) - ? value_types.at(instruction.args[1]) - : Type::builtin("isize"); - std::string index = rhs; - if (instruction.text == "-") { - index = "%af.ptr.neg." + std::to_string(temp_counter++); - os << " " << index << " = sub " << llvm_type(index_type) << " 0, " - << rhs << "\n"; - } - const Type element = instruction.type.pointee ? *instruction.type.pointee - : Type::builtin("u8"); - os << " " << result << " = getelementptr " << llvm_type(element) - << ", ptr " << lhs << ", " << llvm_type(index_type) << ' ' << index - << "\n"; - } else if (instruction.text == "!" || instruction.text == "~") { - os << " " << result << " = xor " << llvm_type(instruction.type) << ' ' - << lhs << ", " - << (instruction.type.kind == TypeKind::Bool ? "true" : "-1") << "\n"; - } else { - const std::string rhs = value_of(instruction.args[1]); - os << " " << result << " = " - << arithmetic_opcode(instruction.type, instruction.text) << ' ' - << llvm_type(instruction.type) << ' ' << lhs << ", " << rhs << "\n"; - } - store_result(instruction, result); - break; - } - case mir::Op::Compare: { - const std::string lhs = value_of(instruction.args[0]); - const std::string rhs = value_of(instruction.args[1]); - const std::string result = "%af.cmp." + std::to_string(temp_counter++); - os << " " << result << " = " - << (instruction.type.is_float() ? "fcmp " : "icmp ") - << compare_predicate(instruction.type, instruction.text) << ' ' - << llvm_type(instruction.type) << ' ' << lhs << ", " << rhs << "\n"; - store_result(instruction, result); - break; - } - case mir::Op::Call: { - if (auto arity = syscall_arity_from_name(instruction.text); - arity && instruction.args.size() == static_cast(*arity) + 1) { - std::unordered_map syscall_values; - for (const auto& arg : instruction.args) - syscall_values[arg] = value_of(arg); - if (emit_syscall( - os, instruction, syscall_values, value_types, temp_counter)) { - if (!instruction.result.empty()) { - const auto it = syscall_values.find(instruction.result); - if (it != syscall_values.end()) - store_result(instruction, it->second); - } - break; - } - } - const auto fit = functions.find(instruction.text); - std::vector parameter_types; - if (fit != functions.end()) - for (const auto& param : fit->second->params) - parameter_types.push_back(param.type); - std::vector args; - for (const auto& arg : instruction.args) - args.push_back(value_of(arg)); - const std::string result = instruction.result.empty() - ? "" - : "%af.call." + std::to_string(temp_counter++); - os << " "; - if (!result.empty()) - os << result << " = "; - os << "call " << llvm_type(instruction.type) << " @" - << llvm_name(instruction.text) << '('; - for (std::size_t i = 0; i < args.size(); ++i) { - if (i) - os << ", "; - const Type arg_type = i < parameter_types.size() - ? parameter_types[i] - : (value_types.contains(instruction.args[i]) - ? value_types[instruction.args[i]] - : Type::builtin("i32")); - os << llvm_type(arg_type) << ' ' << args[i]; - } - os << ")\n"; - if (!result.empty()) - store_result(instruction, result); - break; - } - case mir::Op::Cast: { - const std::string value = value_of(instruction.args[0]); - const Type from = value_types.contains(instruction.args[0]) - ? value_types[instruction.args[0]] - : Type::builtin("i32"); - if (from.kind == TypeKind::Str && instruction.type.kind == TypeKind::RawPtr) { - store_result(instruction, value); - break; - } - std::string opcode; - if (from.kind == TypeKind::RawPtr && instruction.type.is_integer()) - opcode = "ptrtoint"; - else if (from.is_integer() && instruction.type.kind == TypeKind::RawPtr) - opcode = "inttoptr"; - else if (from.is_integer() && instruction.type.is_integer()) { - const int from_bits = integer_bits(from), - to_bits = integer_bits(instruction.type); - opcode = from_bits < to_bits - ? (unsigned_integer(from.kind) ? "zext" : "sext") - : from_bits > to_bits ? "trunc" - : "bitcast"; - } else if (from.kind == TypeKind::Bool && instruction.type.is_integer()) { - opcode = "zext"; - } else if (from.is_integer() && instruction.type.is_float()) - opcode = unsigned_integer(from.kind) ? "uitofp" : "sitofp"; - else if (from.is_float() && instruction.type.is_integer()) - opcode = unsigned_integer(instruction.type.kind) ? "fptoui" : "fptosi"; - else if (from.is_float() && instruction.type.is_float()) - opcode = from.kind == TypeKind::F32 ? "fpext" : "fptrunc"; - else - opcode = "bitcast"; - const std::string result = "%af.cast." + std::to_string(temp_counter++); - os << " " << result << " = " << opcode << ' ' << llvm_type(from) << ' ' - << value << " to " << llvm_type(instruction.type) << "\n"; - store_result(instruction, result); - break; - } - case mir::Op::Await: { - const std::size_t id = ++current_await; - os << " br label %async.await." << id << ".poll\n"; - os << "async.await." << id << ".poll:\n"; - const std::string future = value_of(instruction.args[0]); - const std::string child_state = "%await.state." + std::to_string(id); - const std::string vtable = "%await.vtable." + std::to_string(id); - const std::string poll_addr = "%await.poll.addr." + std::to_string(id); - const std::string drop_addr = "%await.drop.addr." + std::to_string(id); - const std::string poll = "%await.pollfn." + std::to_string(id); - const std::string drop = "%await.dropfn." + std::to_string(id); - os << " " << child_state << " = extractvalue %uinx.future " << future - << ", 0\n"; - os << " " << vtable << " = extractvalue %uinx.future " << future << ", 1\n"; - os << " " << poll_addr << " = getelementptr inbounds %uinx.future.vtable, ptr " - << vtable << ", i32 0, i32 0\n"; - os << " " << drop_addr << " = getelementptr inbounds %uinx.future.vtable, ptr " - << vtable << ", i32 0, i32 1\n"; - os << " " << poll << " = load ptr, ptr " << poll_addr << "\n"; - os << " " << drop << " = load ptr, ptr " << drop_addr << "\n"; - const std::string output = "%await.out." + std::to_string(id); - os << " " << output << " = alloca " << llvm_type(instruction.type) << "\n"; - const std::string rc = "%await.rc." + std::to_string(id); - os << " " << rc << " = call i32 " << poll << "(ptr " << child_state << ", ptr " - << output << ")\n"; - const std::string pending = "%await.pending." + std::to_string(id); - os << " " << pending << " = icmp eq i32 " << rc << ", 0\n"; - os << " br i1 " << pending << ", label %async.await." << id - << ".pending, label %async.await." << id << ".nonpending\n"; - os << "async.await." << id << ".pending:\n"; - os << " store i32 " << id << ", ptr %state.addr\n"; - os << " ret i32 0\n"; - os << "async.await." << id << ".nonpending:\n"; - const std::string failed = "%await.failed." + std::to_string(id); - os << " " << failed << " = icmp slt i32 " << rc << ", 0\n"; - os << " br i1 " << failed << ", label %async.await." << id - << ".error, label %async.await." << id << ".ready\n"; - os << "async.await." << id << ".error:\n ret i32 " << rc << "\n"; - os << "async.await." << id << ".ready:\n"; - const std::string ready = "%await.value." + std::to_string(id); - os << " " << ready << " = load " << llvm_type(instruction.type) << ", ptr " - << output << "\n"; - os << " call void " << drop << "(ptr " << child_state << ")\n"; - os << " store i32 0, ptr %state.addr\n"; - store_result(instruction, ready); - os << " br label %async.await." << id << ".cont\n"; - os << "async.await." << id << ".cont:\n"; - break; - } - case mir::Op::InlineAsm: { - mir::Instruction copy = instruction; - std::unordered_map values; - for (auto& operand : copy.asm_operands) { - if (!operand.value.empty()) - values[operand.value] = value_of(operand.value); - if (!operand.out_slot.empty()) - operand.out_slot = value_of(operand.out_slot); - } - emit_inline_asm(os, copy, values, value_types, asm_counter); - break; - } - case mir::Op::Drop: { - const std::string address = value_of(instruction.args[0]); - os << " call void @" << llvm_name(instruction.type.name + "__drop") << "(ptr " - << address << ")\n"; - break; - } - case mir::Op::Jump: - os << " br label %" << mir_label(instruction.args[0]) << "\n"; - break; - case mir::Op::CondJump: { - const std::string condition = value_of(instruction.args[0]); - os << " br i1 " << condition << ", label %" << mir_label(instruction.args[1]) - << ", label %" << mir_label(instruction.args[2]) << "\n"; - break; - } - case mir::Op::Return: { - if (!instruction.args.empty()) { - const std::string value = value_of(instruction.args[0]); - os << " store " << llvm_type(instruction.type) << ' ' << value - << ", ptr %output\n"; - } - os << " store i32 -1, ptr %state.addr\n"; - os << " ret i32 1\n"; - break; - } - } - } - } - os << "}\n\n"; - - os << "define void @" << name << "__drop(ptr %frame) {\nentry:\n"; - os << " %state.addr = getelementptr inbounds " << frame_type << ", ptr %frame, i32 0, i32 0\n"; - os << " %state = load i32, ptr %state.addr\n"; - if (awaits.empty()) { - os << " br label %free\n"; - } else { - os << " switch i32 %state, label %free ["; - for (const auto& [instruction, id] : awaits) { - (void)instruction; - os << " i32 " << id << ", label %cleanup." << id; - } - os << " ]\n"; - for (const auto& [instruction, id] : awaits) { - os << "cleanup." << id << ":\n"; - if (!instruction->args.empty()) { - const auto fit = fields.find(instruction->args[0]); - if (fit != fields.end()) { - os << " %c.addr." << id << " = getelementptr inbounds " << frame_type - << ", ptr %frame, i32 0, i32 " << fit->second.index << "\n"; - os << " %c.future." << id << " = load %uinx.future, ptr %c.addr." << id - << "\n"; - os << " %c.state." << id << " = extractvalue %uinx.future %c.future." << id - << ", 0\n"; - os << " %c.vtable." << id << " = extractvalue %uinx.future %c.future." << id - << ", 1\n"; - os << " %c.drop.addr." << id - << " = getelementptr inbounds %uinx.future.vtable, ptr %c.vtable." << id - << ", i32 0, i32 1\n"; - os << " %c.drop." << id << " = load ptr, ptr %c.drop.addr." << id << "\n"; - os << " call void %c.drop." << id << "(ptr %c.state." << id << ")\n"; - } - } - os << " br label %free\n"; - } - } - os << "free:\n call void @free(ptr %frame)\n ret void\n}\n\n"; -} - -void LLVMCodegen::emit_function( - std::ostream& os, - const mir::Function& fn, - const std::unordered_map& functions) const { - const std::string return_type = fn.is_async ? "%uinx.future" : llvm_type(fn.result); - const std::string name = llvm_name(fn.name); - if (fn.is_extern) { - os << "declare " << return_type << " @" << name << '('; - for (std::size_t i = 0; i < fn.params.size(); ++i) { - if (i) - os << ", "; - os << llvm_type(fn.params[i].type); - } - os << ")\n\n"; - return; - } - if (fn.is_async) { - emit_async_function(os, fn, functions); - return; - } - - os << "define " << return_type << " @" << name << '('; - for (std::size_t i = 0; i < fn.params.size(); ++i) { - if (i) - os << ", "; - os << llvm_type(fn.params[i].type) << " %arg." << llvm_name(fn.params[i].name); - } - os << ") {\n"; - - std::unordered_map values; - std::unordered_map value_types; - for (const auto& param : fn.params) - value_types["%arg." + param.name] = param.type; - std::size_t asm_counter = 0; - std::size_t bounds_counter = 0; - std::size_t atomic_counter = 0; - for (const auto& block : fn.blocks) { - os << llvm_name(block.label) << ":\n"; - for (const auto& instruction : block.instructions) { - auto value_of = [&](const std::string& value) { - const auto it = values.find(value); - return it == values.end() ? value : it->second; - }; - switch (instruction.op) { - case mir::Op::Alloca: - os << " " << instruction.result << " = alloca " << llvm_type(instruction.type) - << "\n"; - value_types[instruction.result] = Type::raw_ptr(instruction.type, true); - break; - case mir::Op::ConstInt: - values[instruction.result] = llvm_integer_constant(instruction.text); - value_types[instruction.result] = instruction.type; - break; - case mir::Op::ConstFloat: - case mir::Op::ConstBool: - case mir::Op::ConstChar: - values[instruction.result] = instruction.text; - value_types[instruction.result] = instruction.type; - break; - case mir::Op::ConstString: - values[instruction.result] = "getelementptr inbounds (i8, ptr @" + - llvm_name(instruction.text) + ", i64 0)"; - value_types[instruction.result] = instruction.type; - break; - case mir::Op::StructInit: { - if (instruction.args.empty()) { - values[instruction.result] = "zeroinitializer"; - value_types[instruction.result] = instruction.type; - break; - } - std::string aggregate = "poison"; - for (std::size_t index = 0; index < instruction.args.size(); ++index) { - const std::string next = - index + 1 == instruction.args.size() - ? instruction.result - : instruction.result + ".field." + std::to_string(index); - const Type arg_type = value_types.contains(instruction.args[index]) - ? value_types[instruction.args[index]] - : Type::builtin("i32"); - os << " " << next << " = insertvalue " << llvm_type(instruction.type) - << ' ' << aggregate << ", " << llvm_type(arg_type) << ' ' - << value_of(instruction.args[index]) << ", " << index << "\n"; - aggregate = next; - } - values[instruction.result] = instruction.result; - value_types[instruction.result] = instruction.type; - break; - } - case mir::Op::Load: - os << " " << instruction.result << " = load "; - if (instruction.flag) - os << "atomic "; - os << llvm_type(instruction.type) << ", ptr " << value_of(instruction.args[0]); - if (instruction.flag) - os << ' ' << instruction.text << ", align " - << type_alignment(instruction.type); - os << "\n"; - values[instruction.result] = instruction.result; - value_types[instruction.result] = instruction.type; - break; - case mir::Op::Store: - os << " store "; - if (instruction.flag) - os << "atomic "; - os << llvm_type(instruction.type) << ' ' << value_of(instruction.args[0]) - << ", ptr " << value_of(instruction.args[1]); - if (instruction.flag) - os << ' ' << instruction.text << ", align " - << type_alignment(instruction.type); - os << "\n"; - break; - case mir::Op::AtomicRmw: { - const std::string result = "%atomic." + std::to_string(atomic_counter++); - os << " " << result << " = atomicrmw " << atomic_rmw_opcode(instruction.text) - << " ptr " << value_of(instruction.args[0]) << ", " - << llvm_type(instruction.type) << ' ' << value_of(instruction.args[1]) << ' ' - << instruction.args[2] << ", align " << type_alignment(instruction.type) - << "\n"; - break; - } - case mir::Op::Fence: - os << " fence " << instruction.text << "\n"; - break; - case mir::Op::CompilerFence: - os << " fence syncscope(\"singlethread\") " << instruction.text << "\n"; - break; - case mir::Op::FieldAddr: - os << " " << instruction.result << " = getelementptr inbounds " - << llvm_type(instruction.auxiliary_type) << ", ptr " - << value_of(instruction.args[0]) << ", i32 0, i32 " << instruction.text - << "\n"; - values[instruction.result] = instruction.result; - value_types[instruction.result] = Type::raw_ptr(instruction.type, true); - break; - case mir::Op::IndexAddr: { - const std::string data = value_of(instruction.args[0]), - index = value_of(instruction.args[1]), - len = value_of(instruction.args[2]); - const int bits = integer_bits(instruction.auxiliary_type); - const bool is_unsigned = unsigned_integer(instruction.auxiliary_type.kind); - const std::string suffix = std::to_string(bounds_counter++); - std::string index64 = index, ok; - if (bits > 64) { - const std::string len_ext = "%bound.len." + suffix; - os << " " << len_ext << " = zext i64 " << len << " to " - << llvm_type(instruction.auxiliary_type) << "\n"; - const std::string lt = "%bound.lt." + suffix; - os << " " << lt << " = icmp ult " << llvm_type(instruction.auxiliary_type) - << " " << index << ", " << len_ext << "\n"; - ok = lt; - if (!is_unsigned) { - const std::string nonneg = "%bound.nonneg." + suffix; - os << " " << nonneg << " = icmp sge " - << llvm_type(instruction.auxiliary_type) << " " << index << ", 0\n"; - const std::string both = "%bound.ok." + suffix; - os << " " << both << " = and i1 " << nonneg << ", " << lt << "\n"; - ok = both; - } - index64 = "%bound.index." + suffix; - os << " " << index64 << " = trunc " - << llvm_type(instruction.auxiliary_type) << " " << index << " to i64\n"; - } else { - if (bits < 64) { - index64 = "%bound.index." + suffix; - os << " " << index64 << " = " << (is_unsigned ? "zext" : "sext") << " " - << llvm_type(instruction.auxiliary_type) << " " << index - << " to i64\n"; - } - ok = "%bound.ok." + suffix; - os << " " << ok << " = icmp ult i64 " << index64 << ", " << len << "\n"; - } - os << " br i1 " << ok << ", label %bounds.ok." << suffix - << ", label %bounds.fail." << suffix << "\n"; - os << "bounds.fail." << suffix - << ":\n call void @llvm.trap()\n unreachable\n"; - os << "bounds.ok." << suffix << ":\n"; - os << " " << instruction.result << " = getelementptr inbounds " - << llvm_type(instruction.type) << ", ptr " << data << ", i64 " << index64 - << "\n"; - values[instruction.result] = instruction.result; - value_types[instruction.result] = Type::raw_ptr(instruction.type, true); - break; - } - case mir::Op::AddressOf: - values[instruction.result] = value_of(instruction.args[0]); - value_types[instruction.result] = instruction.type; - break; - case mir::Op::Binary: { - if (instruction.type.kind == TypeKind::RawPtr && - (instruction.text == "+" || instruction.text == "-") && - instruction.args.size() == 2) { - const Type index_type = value_types.contains(instruction.args[1]) - ? value_types.at(instruction.args[1]) - : Type::builtin("isize"); - std::string index = value_of(instruction.args[1]); - if (instruction.text == "-") { - const std::string neg = instruction.result + ".neg"; - os << " " << neg << " = sub " << llvm_type(index_type) << " 0, " - << index << "\n"; - index = neg; - } - const Type element = instruction.type.pointee ? *instruction.type.pointee - : Type::builtin("u8"); - os << " " << instruction.result << " = getelementptr " - << llvm_type(element) << ", ptr " << value_of(instruction.args[0]) - << ", " << llvm_type(index_type) << ' ' << index << "\n"; - } else if (instruction.text == "!" || instruction.text == "~") { - os << " " << instruction.result << " = xor " << llvm_type(instruction.type) - << ' ' << value_of(instruction.args[0]) << ", " - << (instruction.type.kind == TypeKind::Bool ? "true" : "-1") << "\n"; - } else { - os << " " << instruction.result << " = " - << arithmetic_opcode(instruction.type, instruction.text) << ' ' - << llvm_type(instruction.type) << ' ' << value_of(instruction.args[0]) - << ", " << value_of(instruction.args[1]) << "\n"; - } - values[instruction.result] = instruction.result; - value_types[instruction.result] = instruction.type; - break; - } - case mir::Op::Compare: - os << " " << instruction.result << " = " - << (instruction.type.is_float() ? "fcmp " : "icmp ") - << compare_predicate(instruction.type, instruction.text) << ' ' - << llvm_type(instruction.type) << ' ' << value_of(instruction.args[0]) - << ", " << value_of(instruction.args[1]) << "\n"; - values[instruction.result] = instruction.result; - value_types[instruction.result] = Type::builtin("bool"); - break; - case mir::Op::Call: { - if (auto arity = syscall_arity_from_name(instruction.text); - arity && instruction.args.size() == static_cast(*arity) + 1) { - if (emit_syscall(os, instruction, values, value_types, asm_counter)) - break; - } - const auto fit = functions.find(instruction.text); - std::vector params; - if (fit != functions.end()) - for (const auto& param : fit->second->params) - params.push_back(param.type); - - if ((instruction.text == "uinx_volatile_load_u8" || - instruction.text == "uinx_volatile_load_u32" || - instruction.text == "uinx_volatile_load_u64") && - instruction.args.size() == 1) { - os << " " << instruction.result << " = load volatile " - << llvm_type(instruction.type) << ", ptr " - << value_of(instruction.args[0]) << ", align " - << type_alignment(instruction.type) << "\n"; - values[instruction.result] = instruction.result; - value_types[instruction.result] = instruction.type; - break; - } - if ((instruction.text == "uinx_volatile_store_u8" || - instruction.text == "uinx_volatile_store_u32" || - instruction.text == "uinx_volatile_store_u64") && - instruction.args.size() == 2) { - const Type value_type = - params.size() > 1 ? params[1] : Type::builtin("u64"); - os << " store volatile " << llvm_type(value_type) << ' ' - << value_of(instruction.args[1]) << ", ptr " - << value_of(instruction.args[0]) << ", align " - << type_alignment(value_type) << "\n"; - break; - } - if (auto intrinsic = parse_atomic_intrinsic(instruction.text)) { - const std::string& llvm_ty = intrinsic->llvm_type; - const unsigned align = intrinsic->align; - const std::string& rmw = intrinsic->rmw_op; - if (rmw == "load" && instruction.args.size() == 2) { - if (auto order = atomic_order_from_abi(value_of(instruction.args[1])); - order && valid_atomic_load_order(*order)) { - os << " " << instruction.result << " = load atomic " << llvm_ty - << ", ptr " << value_of(instruction.args[0]) << ' ' << *order - << ", align " << align << "\n"; - values[instruction.result] = instruction.result; - value_types[instruction.result] = instruction.type; - break; - } - } - if (rmw == "store" && instruction.args.size() == 3) { - if (auto order = atomic_order_from_abi(value_of(instruction.args[2])); - order && valid_atomic_store_order(*order)) { - os << " store atomic " << llvm_ty << ' ' - << value_of(instruction.args[1]) << ", ptr " - << value_of(instruction.args[0]) << ' ' << *order << ", align " - << align << "\n"; - break; - } - } - if ((rmw == "add" || rmw == "sub" || rmw == "and" || rmw == "or" || - rmw == "xor" || rmw == "xchg") && - instruction.args.size() == 3) { - if (auto order = atomic_order_from_abi(value_of(instruction.args[2])); - order && valid_atomic_rmw_order(*order)) { - os << " " << instruction.result << " = atomicrmw " << rmw - << " ptr " << value_of(instruction.args[0]) << ", " << llvm_ty - << ' ' << value_of(instruction.args[1]) << ' ' << *order - << ", align " << align << "\n"; - values[instruction.result] = instruction.result; - value_types[instruction.result] = instruction.type; - break; - } - } - if (rmw == "cmpxchg" && instruction.args.size() == 5) { - auto success = atomic_order_from_abi(value_of(instruction.args[3])); - auto failure = atomic_order_from_abi(value_of(instruction.args[4])); - if (success && failure && valid_atomic_rmw_order(*success) && - (valid_atomic_load_order(*failure) || *failure == "monotonic")) { - const std::string failure_fixed = - downgrade_cmpxchg_failure(*success, *failure); - const std::string id = std::to_string(atomic_counter++); - const std::string expected = "%cas.expected." + id; - const std::string pair = "%cas.pair." + id; - const std::string old = "%cas.old." + id; - const std::string ok = "%cas.ok." + id; - const std::string result = instruction.result.empty() - ? "%cas.result." + id - : instruction.result; - os << " " << expected << " = load " << llvm_ty << ", ptr " - << value_of(instruction.args[1]) << ", align " << align << "\n"; - os << " " << pair << " = cmpxchg ptr " - << value_of(instruction.args[0]) << ", " << llvm_ty << ' ' - << expected << ", " << llvm_ty << ' ' - << value_of(instruction.args[2]) << ' ' << *success << ' ' - << failure_fixed << ", align " << align << "\n"; - os << " " << old << " = extractvalue { " << llvm_ty << ", i1 } " - << pair << ", 0\n"; - os << " " << ok << " = extractvalue { " << llvm_ty << ", i1 } " - << pair << ", 1\n"; - os << " store " << llvm_ty << ' ' << old << ", ptr " - << value_of(instruction.args[1]) << ", align " << align << "\n"; - os << " " << result << " = zext i1 " << ok << " to i32\n"; - if (!instruction.result.empty()) { - values[instruction.result] = result; - value_types[instruction.result] = instruction.type; - } - break; - } - } - // Invalid ordering falls through to runtime call (fail-closed - // is handled by sema/codegen validation elsewhere). - } - - os << " "; - if (!instruction.result.empty()) - os << instruction.result << " = "; - os << "call " << llvm_type(instruction.type) << " @" - << llvm_name(instruction.text) << '('; - for (std::size_t i = 0; i < instruction.args.size(); ++i) { - if (i) - os << ", "; - const Type arg_type = i < params.size() - ? params[i] - : (value_types.contains(instruction.args[i]) - ? value_types[instruction.args[i]] - : Type::builtin("i32")); - os << llvm_type(arg_type) << ' ' << value_of(instruction.args[i]); - } - os << ")\n"; - if (!instruction.result.empty()) { - values[instruction.result] = instruction.result; - value_types[instruction.result] = instruction.type; - } - break; - } - case mir::Op::Cast: { - const Type from = value_types.contains(instruction.args[0]) - ? value_types[instruction.args[0]] - : Type::builtin("i32"); - if ((from.kind == TypeKind::Str && instruction.type.kind == TypeKind::RawPtr) || - ((from.kind == TypeKind::Ref || from.kind == TypeKind::RawPtr) && - (instruction.type.kind == TypeKind::Ref || - instruction.type.kind == TypeKind::RawPtr))) { - values[instruction.result] = value_of(instruction.args[0]); - value_types[instruction.result] = instruction.type; - break; - } - std::string opcode; - if (from.kind == TypeKind::RawPtr && instruction.type.is_integer()) - opcode = "ptrtoint"; - else if (from.is_integer() && instruction.type.kind == TypeKind::RawPtr) - opcode = "inttoptr"; - else if (from.is_integer() && instruction.type.is_integer()) { - const int from_bits = integer_bits(from), - to_bits = integer_bits(instruction.type); - opcode = from_bits < to_bits - ? (unsigned_integer(from.kind) ? "zext" : "sext") - : from_bits > to_bits ? "trunc" - : "bitcast"; - } else if (from.kind == TypeKind::Bool && instruction.type.is_integer()) { - opcode = "zext"; - } else if (from.is_integer() && instruction.type.is_float()) - opcode = unsigned_integer(from.kind) ? "uitofp" : "sitofp"; - else if (from.is_float() && instruction.type.is_integer()) - opcode = unsigned_integer(instruction.type.kind) ? "fptoui" : "fptosi"; - else if (from.is_float() && instruction.type.is_float()) - opcode = from.kind == TypeKind::F32 ? "fpext" : "fptrunc"; - else - opcode = "bitcast"; - os << " " << instruction.result << " = " << opcode << ' ' << llvm_type(from) - << ' ' << value_of(instruction.args[0]) << " to " - << llvm_type(instruction.type) << "\n"; - values[instruction.result] = instruction.result; - value_types[instruction.result] = instruction.type; - break; - } - case mir::Op::Await: - diags_.error(instruction.range, - "E0602", - "internal: await reached synchronous LLVM lowering"); - break; - case mir::Op::InlineAsm: - emit_inline_asm(os, instruction, values, value_types, asm_counter); - break; - case mir::Op::Drop: - os << " call void @" << llvm_name(instruction.type.name + "__drop") << "(ptr " - << value_of(instruction.args[0]) << ")\n"; - break; - case mir::Op::Jump: - os << " br label %" << llvm_name(instruction.args[0]) << "\n"; - break; - case mir::Op::CondJump: - os << " br i1 " << value_of(instruction.args[0]) << ", label %" - << llvm_name(instruction.args[1]) << ", label %" - << llvm_name(instruction.args[2]) << "\n"; - break; - case mir::Op::Return: - if (instruction.args.empty()) - os << " ret void\n"; - else - os << " ret " << llvm_type(instruction.type) << ' ' - << value_of(instruction.args[0]) << "\n"; - break; - } - } - } - os << "}\n\n"; -} - std::string LLVMCodegen::emit(const mir::Module& module) { std::ostringstream os; os << "; Uinx LLVM IR - generated by JiTianYu391\n"; @@ -1877,5 +268,4 @@ std::string LLVMCodegen::emit(const mir::Module& module) { emit_function(os, function, functions); return os.str(); } - } // namespace uinx diff --git a/src/codegen_asm.cpp b/src/codegen_asm.cpp new file mode 100644 index 0000000..44bd5e1 --- /dev/null +++ b/src/codegen_asm.cpp @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "codegen_internal.hpp" +#include "uinx/codegen.hpp" + +#include +#include +#include +#include +#include +#include + +namespace uinx { +using namespace codegen_detail; + +bool LLVMCodegen::validate_constraint(std::string_view constraint, const SourceRange& range) const { + std::string raw(constraint); + while (!raw.empty() && std::string("=+&~").find(raw.front()) != std::string::npos) + raw.erase(raw.begin()); + if (raw == "memory" || raw == "cc" || raw == "r" || raw == "m" || raw == "i" || raw == "g") + return true; + if (raw.size() >= 2 && raw.front() == '{' && raw.back() == '}') + raw = raw.substr(1, raw.size() - 2); + + static const std::unordered_set x86 = { + "rax", "rbx", "rcx", "rdx", "rsi", "rdi", "rbp", "rsp", "r8", + "r9", "r10", "r11", "r12", "r13", "r14", "r15", "eax", "ebx", + "ecx", "edx", "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", + "xmm7", "xmm8", "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15"}; + auto numbered = [&](char prefix, int max) { + if (raw.size() < 2 || raw.front() != prefix) + return false; + try { + const int n = std::stoi(raw.substr(1)); + return n >= 0 && n <= max; + } catch (...) { + return false; + } + }; + + bool valid = false; + if (target_.arch == "x86_64") { + valid = x86.contains(raw); + } else if (target_.arch == "aarch64") { + valid = numbered('x', 30) || numbered('w', 30) || numbered('v', 31) || raw == "sp"; + } else if (target_.arch == "riscv64") { + valid = numbered('x', 31) || raw == "ra" || raw == "sp" || raw == "gp" || raw == "tp" || + raw == "a0" || raw == "a1" || raw == "a2" || raw == "a3" || raw == "a4" || + raw == "a5" || raw == "a6" || raw == "a7" || raw == "s0" || raw == "s1" || + raw == "t0" || raw == "t1" || raw == "t2"; + } else { + valid = true; + } + if (!valid) { + diags_.error(range, + "E0600", + "asm constraint/register '" + std::string(constraint) + + "' is not valid for target architecture '" + target_.arch + "'"); + } + return valid; +} + +void LLVMCodegen::emit_inline_asm(std::ostream& os, + const mir::Instruction& in, + std::unordered_map& values, + std::unordered_map& value_types, + std::size_t& counter) const { + std::vector outputs; + std::vector inputs; + std::vector clobbers; + for (const auto& operand : in.asm_operands) { + validate_constraint(operand.constraint, in.range); + if (operand.kind == ast::AsmOperand::Kind::Out || + operand.kind == ast::AsmOperand::Kind::InOut) + outputs.push_back(&operand); + else if (operand.kind == ast::AsmOperand::Kind::In) + inputs.push_back(&operand); + else + clobbers.push_back(&operand); + } + + std::vector constraints; + std::vector> call_args; + for (const auto* output : outputs) { + std::string c = output->constraint; + while (!c.empty() && c.front() == '+') + c.erase(c.begin()); + if (c.empty() || c.front() != '=') + c = "=" + c; + constraints.push_back(std::move(c)); + } + for (std::size_t index = 0; index < outputs.size(); ++index) { + const auto* output = outputs[index]; + if (output->kind != ast::AsmOperand::Kind::InOut) + continue; + std::string initial = "undef"; + if (!output->out_slot.empty()) { + initial = "%asm.inout." + std::to_string(counter) + "." + std::to_string(index); + os << " " << initial << " = load " << llvm_type(output->type) << ", ptr " + << output->out_slot << "\n"; + } else if (!output->value.empty()) { + const auto it = values.find(output->value); + initial = it == values.end() ? output->value : it->second; + } + constraints.push_back(std::to_string(index)); + call_args.push_back({output->type, initial}); + } + for (const auto* input : inputs) { + constraints.push_back(input->constraint); + const auto it = values.find(input->value); + call_args.push_back({input->type, it == values.end() ? input->value : it->second}); + } + for (const auto* clobber : clobbers) { + if (clobber->constraint == "memory") + constraints.push_back("~{memory}"); + else if (clobber->constraint == "cc") + constraints.push_back("~{cc}"); + else { + std::string reg = clobber->constraint; + if (reg.front() == '{' && reg.back() == '}') + constraints.push_back("~" + reg); + else + constraints.push_back("~{" + reg + "}"); + } + } + + std::string return_type = "void"; + if (outputs.size() == 1) + return_type = llvm_type(outputs.front()->type); + else if (outputs.size() > 1) { + return_type = "{ "; + for (std::size_t i = 0; i < outputs.size(); ++i) { + if (i) + return_type += ", "; + return_type += llvm_type(outputs[i]->type); + } + return_type += " }"; + } + + std::string constraint_text; + for (std::size_t i = 0; i < constraints.size(); ++i) { + if (i) + constraint_text += ','; + constraint_text += constraints[i]; + } + + const std::string call_result = outputs.empty() ? "" : "%asm.result." + std::to_string(counter); + os << " "; + if (!call_result.empty()) + os << call_result << " = "; + os << "call " << return_type << " asm "; + if (in.flag) + os << "sideeffect "; + os << '"' << escape_asm(in.text) << "\", \"" << escape_asm(constraint_text) << "\"("; + for (std::size_t i = 0; i < call_args.size(); ++i) { + if (i) + os << ", "; + os << llvm_type(call_args[i].first) << ' ' << call_args[i].second; + } + os << ")\n"; + + for (std::size_t i = 0; i < outputs.size(); ++i) { + const auto* output = outputs[i]; + if (output->out_slot.empty()) + continue; + std::string value = call_result; + if (outputs.size() > 1) { + value = "%asm.out." + std::to_string(counter) + "." + std::to_string(i); + os << " " << value << " = extractvalue " << return_type << ' ' << call_result << ", " + << i << "\n"; + } + os << " store " << llvm_type(output->type) << ' ' << value << ", ptr " << output->out_slot + << "\n"; + value_types[output->out_slot] = output->type; + } + ++counter; +} + +// Lowers the compiler-recognized `uinx_syscall0..6` ABI family into a raw +// kernel trap for the current target. Argument 0 is the syscall number, the +// remaining arguments map to the architecture's syscall argument registers. +// Returns false when the target has no known trap sequence so the caller can +// fall back to an ordinary call. +bool LLVMCodegen::emit_syscall(std::ostream& os, + const mir::Instruction& in, + std::unordered_map& values, + std::unordered_map& value_types, + std::size_t& counter) const { + const int arity = static_cast(in.args.size()); + if (arity < 1 || arity > 7) + return false; + + auto value_of = [&](const std::string& value) -> std::string { + const auto it = values.find(value); + return it == values.end() ? value : it->second; + }; + + // Zero-extend each argument into an i64 so register constraints are + // satisfied regardless of the source integer width. + std::vector args; + args.reserve(static_cast(arity)); + for (const auto& arg : in.args) { + const std::string raw = value_of(arg); + const auto type_it = value_types.find(arg); + const Type type = type_it == value_types.end() ? Type::builtin("i64") : type_it->second; + if (type.str() == "i64" || type.str() == "u64" || type.str() == "usize" || + type.str() == "isize") { + args.push_back(raw); + } else { + args.push_back("%syscall.arg." + std::to_string(counter) + "." + + std::to_string(args.size())); + os << " " << args.back() << " = zext " << llvm_type(type) << ' ' << raw << " to i64\n"; + } + } + + std::string constraint_text; + std::string asm_text; + if (target_.arch == "x86_64") { + // rax = number, rdi/rsi/rdx/r10/r8/r9 = args 1..6. Only the registers + // actually consumed are declared; the kernel additionally clobbers rcx + // (return RIP) and r11 (RFLAGS). + static const char* regs[7] = {"rax", "rdi", "rsi", "rdx", "r10", "r8", "r9"}; + std::vector constraints{"={rax}"}; + for (int i = 0; i < arity; ++i) + constraints.push_back(std::string("{") + regs[i] + "}"); + constraints.push_back("~{rcx}"); + constraints.push_back("~{r11}"); + constraints.push_back("~{memory}"); + constraints.push_back("~{cc}"); + for (std::size_t i = 0; i < constraints.size(); ++i) { + if (i) + constraint_text += ','; + constraint_text += constraints[i]; + } + asm_text = "syscall"; + } else if (target_.arch == "aarch64") { + // x8 = number, x0..x5 = args 1..6. The kernel may clobber x16/x17. + static const char* regs[7] = {"x8", "x0", "x1", "x2", "x3", "x4", "x5"}; + std::vector constraints{"={x0}"}; + for (int i = 0; i < arity; ++i) + constraints.push_back(std::string("{") + regs[i] + "}"); + constraints.push_back("~{x16}"); + constraints.push_back("~{x17}"); + constraints.push_back("~{memory}"); + constraints.push_back("~{cc}"); + for (std::size_t i = 0; i < constraints.size(); ++i) { + if (i) + constraint_text += ','; + constraint_text += constraints[i]; + } + asm_text = "svc #0"; + } else if (target_.arch == "riscv64") { + // a7 = number, a0..a5 = args 1..6. The kernel may clobber a6. + static const char* regs[7] = {"a7", "a0", "a1", "a2", "a3", "a4", "a5"}; + std::vector constraints{"={a0}"}; + for (int i = 0; i < arity; ++i) + constraints.push_back(std::string("{") + regs[i] + "}"); + constraints.push_back("~{a6}"); + constraints.push_back("~{memory}"); + constraints.push_back("~{cc}"); + for (std::size_t i = 0; i < constraints.size(); ++i) { + if (i) + constraint_text += ','; + constraint_text += constraints[i]; + } + asm_text = "ecall"; + } else { + diags_.error(in.range, + "E0601", + "no syscall trap sequence is defined for target architecture '" + + target_.arch + "'"); + return false; + } + + const std::string call_result = "%syscall.result." + std::to_string(counter); + os << " " << call_result << " = call i64 asm sideeffect \"" << asm_text << "\", \"" + << constraint_text << "\"("; + for (std::size_t i = 0; i < args.size(); ++i) { + if (i) + os << ", "; + os << "i64 " << args[i]; + } + os << ")\n"; + + if (!in.result.empty()) { + // The trap returns i64; narrow to the declared result type when needed. + const std::string result_type = llvm_type(in.type); + if (result_type == "i64") { + values[in.result] = call_result; + } else { + const std::string truncated = "%syscall.trunc." + std::to_string(counter); + os << " " << truncated << " = trunc i64 " << call_result << " to " << result_type + << "\n"; + values[in.result] = truncated; + } + value_types[in.result] = in.type; + } + ++counter; + return true; +} +} // namespace uinx diff --git a/src/codegen_async.cpp b/src/codegen_async.cpp new file mode 100644 index 0000000..4e715fc --- /dev/null +++ b/src/codegen_async.cpp @@ -0,0 +1,556 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "codegen_internal.hpp" +#include "uinx/codegen.hpp" + +#include +#include +#include +#include +#include +#include + +namespace uinx { +using namespace codegen_detail; + +void LLVMCodegen::emit_async_function( + std::ostream& os, + const mir::Function& fn, + const std::unordered_map& functions) const { + struct FrameField { + Type type; + std::size_t index{}; + bool address{false}; + }; + + std::unordered_map fields; + std::vector> ordered; + std::size_t next_field = 1; + auto add_field = [&](const std::string& key, Type type, bool address) { + if (key.empty() || fields.contains(key)) + return; + FrameField field{std::move(type), next_field++, address}; + fields.emplace(key, field); + ordered.push_back({key, field}); + }; + + for (const auto& param : fn.params) + add_field("%arg." + param.name, param.type, false); + std::vector> awaits; + std::size_t await_count = 0; + for (const auto& block : fn.blocks) { + for (const auto& instruction : block.instructions) { + if (instruction.op == mir::Op::Await) + awaits.push_back({&instruction, ++await_count}); + if (instruction.result.empty()) + continue; + if (instruction.op == mir::Op::Alloca) + add_field(instruction.result, instruction.type, true); + else if (instruction.op == mir::Op::FieldAddr || instruction.op == mir::Op::IndexAddr) + add_field(instruction.result, Type::raw_ptr(instruction.type, true), false); + else + add_field(instruction.result, instruction.type, false); + } + } + + const std::string name = llvm_name(fn.name); + const std::string frame_type = "%uinx.async.frame." + name; + os << frame_type << " = type { i32"; + for (const auto& [key, field] : ordered) { + (void)key; + os << ", " << llvm_type(field.type); + } + os << " }\n"; + os << "@" << name << "__vtable = private constant %uinx.future.vtable { ptr @" << name + << "__poll, ptr @" << name << "__drop }\n"; + + os << "define %uinx.future @" << name << '('; + for (std::size_t i = 0; i < fn.params.size(); ++i) { + if (i) + os << ", "; + os << llvm_type(fn.params[i].type) << " %arg." << llvm_name(fn.params[i].name); + } + os << ") {\nentry:\n"; + os << " %frame.size.ptr = getelementptr " << frame_type << ", ptr null, i32 1\n"; + os << " %frame.size = ptrtoint ptr %frame.size.ptr to i64\n"; + os << " %frame = call ptr @malloc(i64 %frame.size)\n"; + os << " %oom = icmp eq ptr %frame, null\n"; + os << " br i1 %oom, label %async.oom, label %async.init\n"; + os << "async.oom:\n call void @abort()\n unreachable\n"; + os << "async.init:\n"; + os << " %state.addr = getelementptr inbounds " << frame_type << ", ptr %frame, i32 0, i32 0\n"; + os << " store i32 0, ptr %state.addr\n"; + for (const auto& param : fn.params) { + const auto& field = fields.at("%arg." + param.name); + os << " %param.addr." << field.index << " = getelementptr inbounds " << frame_type + << ", ptr %frame, i32 0, i32 " << field.index << "\n"; + os << " store " << llvm_type(param.type) << " %arg." << llvm_name(param.name) + << ", ptr %param.addr." << field.index << "\n"; + } + os << " %future.0 = insertvalue %uinx.future poison, ptr %frame, 0\n"; + os << " %future.1 = insertvalue %uinx.future %future.0, ptr @" << name << "__vtable, 1\n"; + os << " ret %uinx.future %future.1\n}\n\n"; + + os << "define i32 @" << name << "__poll(ptr %frame, ptr %output) {\nentry:\n"; + os << " %state.addr = getelementptr inbounds " << frame_type << ", ptr %frame, i32 0, i32 0\n"; + os << " %state = load i32, ptr %state.addr\n"; + const std::string first_label = fn.blocks.empty() ? "entry" : fn.blocks.front().label; + os << " switch i32 %state, label %async.invalid [ i32 0, label %mir." + << llvm_name(first_label); + for (const auto& [instruction, id] : awaits) { + (void)instruction; + os << " i32 " << id << ", label %async.resume." << id; + } + os << " i32 -1, label %async.completed ]\n"; + os << "async.invalid:\n ret i32 -22\n"; + os << "async.completed:\n ret i32 1\n"; + for (const auto& [instruction, id] : awaits) { + (void)instruction; + os << "async.resume." << id << ":\n br label %async.await." << id << ".poll\n"; + } + + std::size_t temp_counter = 0; + std::size_t asm_counter = 0; + std::size_t bounds_counter = 0; + std::size_t current_await = 0; + std::unordered_map value_types; + for (const auto& [key, field] : ordered) + value_types[key] = field.type; + + auto frame_addr = [&](const std::string& key) -> std::string { + const auto it = fields.find(key); + if (it == fields.end()) + return key; + const std::string temp = "%af.addr." + std::to_string(temp_counter++); + os << " " << temp << " = getelementptr inbounds " << frame_type + << ", ptr %frame, i32 0, i32 " << it->second.index << "\n"; + return temp; + }; + auto value_of = [&](const std::string& value) -> std::string { + const auto it = fields.find(value); + if (it == fields.end()) + return value; + const std::string address = frame_addr(value); + if (it->second.address) + return address; + const std::string temp = "%af.load." + std::to_string(temp_counter++); + os << " " << temp << " = load " << llvm_type(it->second.type) << ", ptr " << address + << "\n"; + return temp; + }; + auto store_result = [&](const mir::Instruction& instruction, const std::string& value) { + if (instruction.result.empty()) + return; + const auto it = fields.find(instruction.result); + if (it == fields.end() || it->second.address) + return; + const std::string address = frame_addr(instruction.result); + os << " store " << llvm_type(it->second.type) << ' ' << value << ", ptr " << address + << "\n"; + }; + auto mir_label = [&](const std::string& label) { return "mir." + llvm_name(label); }; + + for (const auto& block : fn.blocks) { + os << mir_label(block.label) << ":\n"; + for (const auto& instruction : block.instructions) { + switch (instruction.op) { + case mir::Op::Alloca: + break; + case mir::Op::ConstInt: + store_result(instruction, llvm_integer_constant(instruction.text)); + break; + case mir::Op::ConstFloat: + case mir::Op::ConstBool: + case mir::Op::ConstChar: + store_result(instruction, instruction.text); + break; + case mir::Op::ConstString: { + store_result(instruction, + "getelementptr inbounds (i8, ptr @" + llvm_name(instruction.text) + + ", i64 0)"); + break; + } + case mir::Op::StructInit: { + if (instruction.args.empty()) { + store_result(instruction, "zeroinitializer"); + break; + } + std::string aggregate = "poison"; + for (std::size_t index = 0; index < instruction.args.size(); ++index) { + const std::string arg = value_of(instruction.args[index]); + const Type arg_type = value_types.contains(instruction.args[index]) + ? value_types[instruction.args[index]] + : Type::builtin("i32"); + const std::string next = "%af.insert." + std::to_string(temp_counter++); + os << " " << next << " = insertvalue " << llvm_type(instruction.type) + << ' ' << aggregate << ", " << llvm_type(arg_type) << ' ' << arg << ", " + << index << "\n"; + aggregate = next; + } + store_result(instruction, aggregate); + break; + } + case mir::Op::Load: { + const std::string address = value_of(instruction.args[0]); + const std::string result = "%af.op." + std::to_string(temp_counter++); + os << " " << result << " = load "; + if (instruction.flag) + os << "atomic "; + os << llvm_type(instruction.type) << ", ptr " << address; + if (instruction.flag) + os << ' ' << instruction.text << ", align " + << type_alignment(instruction.type); + os << "\n"; + store_result(instruction, result); + break; + } + case mir::Op::Store: { + const std::string value = value_of(instruction.args[0]); + const std::string address = value_of(instruction.args[1]); + os << " store "; + if (instruction.flag) + os << "atomic "; + os << llvm_type(instruction.type) << ' ' << value << ", ptr " << address; + if (instruction.flag) + os << ' ' << instruction.text << ", align " + << type_alignment(instruction.type); + os << "\n"; + break; + } + case mir::Op::AtomicRmw: { + const std::string address = value_of(instruction.args[0]); + const std::string value = value_of(instruction.args[1]); + const std::string result = "%af.atomic." + std::to_string(temp_counter++); + os << " " << result << " = atomicrmw " << atomic_rmw_opcode(instruction.text) + << " ptr " << address << ", " << llvm_type(instruction.type) << ' ' << value + << ' ' << instruction.args[2] << ", align " + << type_alignment(instruction.type) << "\n"; + break; + } + case mir::Op::Fence: + os << " fence " << instruction.text << "\n"; + break; + case mir::Op::CompilerFence: + os << " fence syncscope(\"singlethread\") " << instruction.text << "\n"; + break; + case mir::Op::FieldAddr: { + const std::string base = value_of(instruction.args[0]); + const std::string result = "%af.field." + std::to_string(temp_counter++); + os << " " << result << " = getelementptr inbounds " + << llvm_type(instruction.auxiliary_type) << ", ptr " << base + << ", i32 0, i32 " << instruction.text << "\n"; + store_result(instruction, result); + break; + } + case mir::Op::IndexAddr: { + const std::string data = value_of(instruction.args[0]), + index = value_of(instruction.args[1]), + len = value_of(instruction.args[2]); + const int bits = integer_bits(instruction.auxiliary_type); + const bool is_unsigned = unsigned_integer(instruction.auxiliary_type.kind); + const std::string suffix = std::to_string(bounds_counter++); + std::string index64 = index, ok; + if (bits > 64) { + const std::string len_ext = "%af.bound.len." + suffix; + os << " " << len_ext << " = zext i64 " << len << " to " + << llvm_type(instruction.auxiliary_type) << "\n"; + const std::string lt = "%af.bound.lt." + suffix; + os << " " << lt << " = icmp ult " << llvm_type(instruction.auxiliary_type) + << " " << index << ", " << len_ext << "\n"; + ok = lt; + if (!is_unsigned) { + const std::string nonneg = "%af.bound.nonneg." + suffix; + os << " " << nonneg << " = icmp sge " + << llvm_type(instruction.auxiliary_type) << " " << index << ", 0\n"; + const std::string both = "%af.bound.ok." + suffix; + os << " " << both << " = and i1 " << nonneg << ", " << lt << "\n"; + ok = both; + } + index64 = "%af.bound.index." + suffix; + os << " " << index64 << " = trunc " + << llvm_type(instruction.auxiliary_type) << " " << index << " to i64\n"; + } else { + if (bits < 64) { + index64 = "%af.bound.index." + suffix; + os << " " << index64 << " = " << (is_unsigned ? "zext" : "sext") << " " + << llvm_type(instruction.auxiliary_type) << " " << index + << " to i64\n"; + } + ok = "%af.bound.ok." + suffix; + os << " " << ok << " = icmp ult i64 " << index64 << ", " << len << "\n"; + } + os << " br i1 " << ok << ", label %async.bounds.ok." << suffix + << ", label %async.bounds.fail." << suffix << "\n"; + os << "async.bounds.fail." << suffix + << ":\n call void @llvm.trap()\n unreachable\n"; + os << "async.bounds.ok." << suffix << ":\n"; + const std::string result = "%af.index." + suffix; + os << " " << result << " = getelementptr inbounds " + << llvm_type(instruction.type) << ", ptr " << data << ", i64 " << index64 + << "\n"; + store_result(instruction, result); + break; + } + case mir::Op::AddressOf: + store_result(instruction, value_of(instruction.args[0])); + break; + case mir::Op::Binary: { + const std::string lhs = value_of(instruction.args[0]); + const std::string result = "%af.bin." + std::to_string(temp_counter++); + if (instruction.type.kind == TypeKind::RawPtr && + (instruction.text == "+" || instruction.text == "-") && + instruction.args.size() == 2) { + const std::string rhs = value_of(instruction.args[1]); + const Type index_type = value_types.contains(instruction.args[1]) + ? value_types.at(instruction.args[1]) + : Type::builtin("isize"); + std::string index = rhs; + if (instruction.text == "-") { + index = "%af.ptr.neg." + std::to_string(temp_counter++); + os << " " << index << " = sub " << llvm_type(index_type) << " 0, " + << rhs << "\n"; + } + const Type element = instruction.type.pointee ? *instruction.type.pointee + : Type::builtin("u8"); + os << " " << result << " = getelementptr " << llvm_type(element) + << ", ptr " << lhs << ", " << llvm_type(index_type) << ' ' << index + << "\n"; + } else if (instruction.text == "!" || instruction.text == "~") { + os << " " << result << " = xor " << llvm_type(instruction.type) << ' ' + << lhs << ", " + << (instruction.type.kind == TypeKind::Bool ? "true" : "-1") << "\n"; + } else { + const std::string rhs = value_of(instruction.args[1]); + os << " " << result << " = " + << arithmetic_opcode(instruction.type, instruction.text) << ' ' + << llvm_type(instruction.type) << ' ' << lhs << ", " << rhs << "\n"; + } + store_result(instruction, result); + break; + } + case mir::Op::Compare: { + const std::string lhs = value_of(instruction.args[0]); + const std::string rhs = value_of(instruction.args[1]); + const std::string result = "%af.cmp." + std::to_string(temp_counter++); + os << " " << result << " = " + << (instruction.type.is_float() ? "fcmp " : "icmp ") + << compare_predicate(instruction.type, instruction.text) << ' ' + << llvm_type(instruction.type) << ' ' << lhs << ", " << rhs << "\n"; + store_result(instruction, result); + break; + } + case mir::Op::Call: { + if (auto arity = syscall_arity_from_name(instruction.text); + arity && instruction.args.size() == static_cast(*arity) + 1) { + std::unordered_map syscall_values; + for (const auto& arg : instruction.args) + syscall_values[arg] = value_of(arg); + if (emit_syscall( + os, instruction, syscall_values, value_types, temp_counter)) { + if (!instruction.result.empty()) { + const auto it = syscall_values.find(instruction.result); + if (it != syscall_values.end()) + store_result(instruction, it->second); + } + break; + } + } + const auto fit = functions.find(instruction.text); + std::vector parameter_types; + if (fit != functions.end()) + for (const auto& param : fit->second->params) + parameter_types.push_back(param.type); + std::vector args; + for (const auto& arg : instruction.args) + args.push_back(value_of(arg)); + const std::string result = instruction.result.empty() + ? "" + : "%af.call." + std::to_string(temp_counter++); + os << " "; + if (!result.empty()) + os << result << " = "; + os << "call " << llvm_type(instruction.type) << " @" + << llvm_name(instruction.text) << '('; + for (std::size_t i = 0; i < args.size(); ++i) { + if (i) + os << ", "; + const Type arg_type = i < parameter_types.size() + ? parameter_types[i] + : (value_types.contains(instruction.args[i]) + ? value_types[instruction.args[i]] + : Type::builtin("i32")); + os << llvm_type(arg_type) << ' ' << args[i]; + } + os << ")\n"; + if (!result.empty()) + store_result(instruction, result); + break; + } + case mir::Op::Cast: { + const std::string value = value_of(instruction.args[0]); + const Type from = value_types.contains(instruction.args[0]) + ? value_types[instruction.args[0]] + : Type::builtin("i32"); + if (from.kind == TypeKind::Str && instruction.type.kind == TypeKind::RawPtr) { + store_result(instruction, value); + break; + } + std::string opcode; + if (from.kind == TypeKind::RawPtr && instruction.type.is_integer()) + opcode = "ptrtoint"; + else if (from.is_integer() && instruction.type.kind == TypeKind::RawPtr) + opcode = "inttoptr"; + else if (from.is_integer() && instruction.type.is_integer()) { + const int from_bits = integer_bits(from), + to_bits = integer_bits(instruction.type); + opcode = from_bits < to_bits + ? (unsigned_integer(from.kind) ? "zext" : "sext") + : from_bits > to_bits ? "trunc" + : "bitcast"; + } else if (from.kind == TypeKind::Bool && instruction.type.is_integer()) { + opcode = "zext"; + } else if (from.is_integer() && instruction.type.is_float()) + opcode = unsigned_integer(from.kind) ? "uitofp" : "sitofp"; + else if (from.is_float() && instruction.type.is_integer()) + opcode = unsigned_integer(instruction.type.kind) ? "fptoui" : "fptosi"; + else if (from.is_float() && instruction.type.is_float()) + opcode = from.kind == TypeKind::F32 ? "fpext" : "fptrunc"; + else + opcode = "bitcast"; + const std::string result = "%af.cast." + std::to_string(temp_counter++); + os << " " << result << " = " << opcode << ' ' << llvm_type(from) << ' ' + << value << " to " << llvm_type(instruction.type) << "\n"; + store_result(instruction, result); + break; + } + case mir::Op::Await: { + const std::size_t id = ++current_await; + os << " br label %async.await." << id << ".poll\n"; + os << "async.await." << id << ".poll:\n"; + const std::string future = value_of(instruction.args[0]); + const std::string child_state = "%await.state." + std::to_string(id); + const std::string vtable = "%await.vtable." + std::to_string(id); + const std::string poll_addr = "%await.poll.addr." + std::to_string(id); + const std::string drop_addr = "%await.drop.addr." + std::to_string(id); + const std::string poll = "%await.pollfn." + std::to_string(id); + const std::string drop = "%await.dropfn." + std::to_string(id); + os << " " << child_state << " = extractvalue %uinx.future " << future + << ", 0\n"; + os << " " << vtable << " = extractvalue %uinx.future " << future << ", 1\n"; + os << " " << poll_addr << " = getelementptr inbounds %uinx.future.vtable, ptr " + << vtable << ", i32 0, i32 0\n"; + os << " " << drop_addr << " = getelementptr inbounds %uinx.future.vtable, ptr " + << vtable << ", i32 0, i32 1\n"; + os << " " << poll << " = load ptr, ptr " << poll_addr << "\n"; + os << " " << drop << " = load ptr, ptr " << drop_addr << "\n"; + const std::string output = "%await.out." + std::to_string(id); + os << " " << output << " = alloca " << llvm_type(instruction.type) << "\n"; + const std::string rc = "%await.rc." + std::to_string(id); + os << " " << rc << " = call i32 " << poll << "(ptr " << child_state << ", ptr " + << output << ")\n"; + const std::string pending = "%await.pending." + std::to_string(id); + os << " " << pending << " = icmp eq i32 " << rc << ", 0\n"; + os << " br i1 " << pending << ", label %async.await." << id + << ".pending, label %async.await." << id << ".nonpending\n"; + os << "async.await." << id << ".pending:\n"; + os << " store i32 " << id << ", ptr %state.addr\n"; + os << " ret i32 0\n"; + os << "async.await." << id << ".nonpending:\n"; + const std::string failed = "%await.failed." + std::to_string(id); + os << " " << failed << " = icmp slt i32 " << rc << ", 0\n"; + os << " br i1 " << failed << ", label %async.await." << id + << ".error, label %async.await." << id << ".ready\n"; + os << "async.await." << id << ".error:\n ret i32 " << rc << "\n"; + os << "async.await." << id << ".ready:\n"; + const std::string ready = "%await.value." + std::to_string(id); + os << " " << ready << " = load " << llvm_type(instruction.type) << ", ptr " + << output << "\n"; + os << " call void " << drop << "(ptr " << child_state << ")\n"; + os << " store i32 0, ptr %state.addr\n"; + store_result(instruction, ready); + os << " br label %async.await." << id << ".cont\n"; + os << "async.await." << id << ".cont:\n"; + break; + } + case mir::Op::InlineAsm: { + mir::Instruction copy = instruction; + std::unordered_map values; + for (auto& operand : copy.asm_operands) { + if (!operand.value.empty()) + values[operand.value] = value_of(operand.value); + if (!operand.out_slot.empty()) + operand.out_slot = value_of(operand.out_slot); + } + emit_inline_asm(os, copy, values, value_types, asm_counter); + break; + } + case mir::Op::Drop: { + const std::string address = value_of(instruction.args[0]); + os << " call void @" << llvm_name(instruction.type.name + "__drop") << "(ptr " + << address << ")\n"; + break; + } + case mir::Op::Jump: + os << " br label %" << mir_label(instruction.args[0]) << "\n"; + break; + case mir::Op::CondJump: { + const std::string condition = value_of(instruction.args[0]); + os << " br i1 " << condition << ", label %" << mir_label(instruction.args[1]) + << ", label %" << mir_label(instruction.args[2]) << "\n"; + break; + } + case mir::Op::Return: { + if (!instruction.args.empty()) { + const std::string value = value_of(instruction.args[0]); + os << " store " << llvm_type(instruction.type) << ' ' << value + << ", ptr %output\n"; + } + os << " store i32 -1, ptr %state.addr\n"; + os << " ret i32 1\n"; + break; + } + } + } + } + os << "}\n\n"; + + os << "define void @" << name << "__drop(ptr %frame) {\nentry:\n"; + os << " %state.addr = getelementptr inbounds " << frame_type << ", ptr %frame, i32 0, i32 0\n"; + os << " %state = load i32, ptr %state.addr\n"; + if (awaits.empty()) { + os << " br label %free\n"; + } else { + os << " switch i32 %state, label %free ["; + for (const auto& [instruction, id] : awaits) { + (void)instruction; + os << " i32 " << id << ", label %cleanup." << id; + } + os << " ]\n"; + for (const auto& [instruction, id] : awaits) { + os << "cleanup." << id << ":\n"; + if (!instruction->args.empty()) { + const auto fit = fields.find(instruction->args[0]); + if (fit != fields.end()) { + os << " %c.addr." << id << " = getelementptr inbounds " << frame_type + << ", ptr %frame, i32 0, i32 " << fit->second.index << "\n"; + os << " %c.future." << id << " = load %uinx.future, ptr %c.addr." << id + << "\n"; + os << " %c.state." << id << " = extractvalue %uinx.future %c.future." << id + << ", 0\n"; + os << " %c.vtable." << id << " = extractvalue %uinx.future %c.future." << id + << ", 1\n"; + os << " %c.drop.addr." << id + << " = getelementptr inbounds %uinx.future.vtable, ptr %c.vtable." << id + << ", i32 0, i32 1\n"; + os << " %c.drop." << id << " = load ptr, ptr %c.drop.addr." << id << "\n"; + os << " call void %c.drop." << id << "(ptr %c.state." << id << ")\n"; + } + } + os << " br label %free\n"; + } + } + os << "free:\n call void @free(ptr %frame)\n ret void\n}\n\n"; +} +} // namespace uinx diff --git a/src/codegen_function.cpp b/src/codegen_function.cpp new file mode 100644 index 0000000..2786eea --- /dev/null +++ b/src/codegen_function.cpp @@ -0,0 +1,457 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "codegen_internal.hpp" +#include "uinx/codegen.hpp" + +#include +#include +#include +#include +#include +#include + +namespace uinx { +using namespace codegen_detail; + +void LLVMCodegen::emit_function( + std::ostream& os, + const mir::Function& fn, + const std::unordered_map& functions) const { + const std::string return_type = fn.is_async ? "%uinx.future" : llvm_type(fn.result); + const std::string name = llvm_name(fn.name); + if (fn.is_extern) { + os << "declare " << return_type << " @" << name << '('; + for (std::size_t i = 0; i < fn.params.size(); ++i) { + if (i) + os << ", "; + os << llvm_type(fn.params[i].type); + } + os << ")\n\n"; + return; + } + if (fn.is_async) { + emit_async_function(os, fn, functions); + return; + } + + os << "define " << return_type << " @" << name << '('; + for (std::size_t i = 0; i < fn.params.size(); ++i) { + if (i) + os << ", "; + os << llvm_type(fn.params[i].type) << " %arg." << llvm_name(fn.params[i].name); + } + os << ") {\n"; + + std::unordered_map values; + std::unordered_map value_types; + for (const auto& param : fn.params) + value_types["%arg." + param.name] = param.type; + std::size_t asm_counter = 0; + std::size_t bounds_counter = 0; + std::size_t atomic_counter = 0; + for (const auto& block : fn.blocks) { + os << llvm_name(block.label) << ":\n"; + for (const auto& instruction : block.instructions) { + auto value_of = [&](const std::string& value) { + const auto it = values.find(value); + return it == values.end() ? value : it->second; + }; + switch (instruction.op) { + case mir::Op::Alloca: + os << " " << instruction.result << " = alloca " << llvm_type(instruction.type) + << "\n"; + value_types[instruction.result] = Type::raw_ptr(instruction.type, true); + break; + case mir::Op::ConstInt: + values[instruction.result] = llvm_integer_constant(instruction.text); + value_types[instruction.result] = instruction.type; + break; + case mir::Op::ConstFloat: + case mir::Op::ConstBool: + case mir::Op::ConstChar: + values[instruction.result] = instruction.text; + value_types[instruction.result] = instruction.type; + break; + case mir::Op::ConstString: + values[instruction.result] = "getelementptr inbounds (i8, ptr @" + + llvm_name(instruction.text) + ", i64 0)"; + value_types[instruction.result] = instruction.type; + break; + case mir::Op::StructInit: { + if (instruction.args.empty()) { + values[instruction.result] = "zeroinitializer"; + value_types[instruction.result] = instruction.type; + break; + } + std::string aggregate = "poison"; + for (std::size_t index = 0; index < instruction.args.size(); ++index) { + const std::string next = + index + 1 == instruction.args.size() + ? instruction.result + : instruction.result + ".field." + std::to_string(index); + const Type arg_type = value_types.contains(instruction.args[index]) + ? value_types[instruction.args[index]] + : Type::builtin("i32"); + os << " " << next << " = insertvalue " << llvm_type(instruction.type) + << ' ' << aggregate << ", " << llvm_type(arg_type) << ' ' + << value_of(instruction.args[index]) << ", " << index << "\n"; + aggregate = next; + } + values[instruction.result] = instruction.result; + value_types[instruction.result] = instruction.type; + break; + } + case mir::Op::Load: + os << " " << instruction.result << " = load "; + if (instruction.flag) + os << "atomic "; + os << llvm_type(instruction.type) << ", ptr " << value_of(instruction.args[0]); + if (instruction.flag) + os << ' ' << instruction.text << ", align " + << type_alignment(instruction.type); + os << "\n"; + values[instruction.result] = instruction.result; + value_types[instruction.result] = instruction.type; + break; + case mir::Op::Store: + os << " store "; + if (instruction.flag) + os << "atomic "; + os << llvm_type(instruction.type) << ' ' << value_of(instruction.args[0]) + << ", ptr " << value_of(instruction.args[1]); + if (instruction.flag) + os << ' ' << instruction.text << ", align " + << type_alignment(instruction.type); + os << "\n"; + break; + case mir::Op::AtomicRmw: { + const std::string result = "%atomic." + std::to_string(atomic_counter++); + os << " " << result << " = atomicrmw " << atomic_rmw_opcode(instruction.text) + << " ptr " << value_of(instruction.args[0]) << ", " + << llvm_type(instruction.type) << ' ' << value_of(instruction.args[1]) << ' ' + << instruction.args[2] << ", align " << type_alignment(instruction.type) + << "\n"; + break; + } + case mir::Op::Fence: + os << " fence " << instruction.text << "\n"; + break; + case mir::Op::CompilerFence: + os << " fence syncscope(\"singlethread\") " << instruction.text << "\n"; + break; + case mir::Op::FieldAddr: + os << " " << instruction.result << " = getelementptr inbounds " + << llvm_type(instruction.auxiliary_type) << ", ptr " + << value_of(instruction.args[0]) << ", i32 0, i32 " << instruction.text + << "\n"; + values[instruction.result] = instruction.result; + value_types[instruction.result] = Type::raw_ptr(instruction.type, true); + break; + case mir::Op::IndexAddr: { + const std::string data = value_of(instruction.args[0]), + index = value_of(instruction.args[1]), + len = value_of(instruction.args[2]); + const int bits = integer_bits(instruction.auxiliary_type); + const bool is_unsigned = unsigned_integer(instruction.auxiliary_type.kind); + const std::string suffix = std::to_string(bounds_counter++); + std::string index64 = index, ok; + if (bits > 64) { + const std::string len_ext = "%bound.len." + suffix; + os << " " << len_ext << " = zext i64 " << len << " to " + << llvm_type(instruction.auxiliary_type) << "\n"; + const std::string lt = "%bound.lt." + suffix; + os << " " << lt << " = icmp ult " << llvm_type(instruction.auxiliary_type) + << " " << index << ", " << len_ext << "\n"; + ok = lt; + if (!is_unsigned) { + const std::string nonneg = "%bound.nonneg." + suffix; + os << " " << nonneg << " = icmp sge " + << llvm_type(instruction.auxiliary_type) << " " << index << ", 0\n"; + const std::string both = "%bound.ok." + suffix; + os << " " << both << " = and i1 " << nonneg << ", " << lt << "\n"; + ok = both; + } + index64 = "%bound.index." + suffix; + os << " " << index64 << " = trunc " + << llvm_type(instruction.auxiliary_type) << " " << index << " to i64\n"; + } else { + if (bits < 64) { + index64 = "%bound.index." + suffix; + os << " " << index64 << " = " << (is_unsigned ? "zext" : "sext") << " " + << llvm_type(instruction.auxiliary_type) << " " << index + << " to i64\n"; + } + ok = "%bound.ok." + suffix; + os << " " << ok << " = icmp ult i64 " << index64 << ", " << len << "\n"; + } + os << " br i1 " << ok << ", label %bounds.ok." << suffix + << ", label %bounds.fail." << suffix << "\n"; + os << "bounds.fail." << suffix + << ":\n call void @llvm.trap()\n unreachable\n"; + os << "bounds.ok." << suffix << ":\n"; + os << " " << instruction.result << " = getelementptr inbounds " + << llvm_type(instruction.type) << ", ptr " << data << ", i64 " << index64 + << "\n"; + values[instruction.result] = instruction.result; + value_types[instruction.result] = Type::raw_ptr(instruction.type, true); + break; + } + case mir::Op::AddressOf: + values[instruction.result] = value_of(instruction.args[0]); + value_types[instruction.result] = instruction.type; + break; + case mir::Op::Binary: { + if (instruction.type.kind == TypeKind::RawPtr && + (instruction.text == "+" || instruction.text == "-") && + instruction.args.size() == 2) { + const Type index_type = value_types.contains(instruction.args[1]) + ? value_types.at(instruction.args[1]) + : Type::builtin("isize"); + std::string index = value_of(instruction.args[1]); + if (instruction.text == "-") { + const std::string neg = instruction.result + ".neg"; + os << " " << neg << " = sub " << llvm_type(index_type) << " 0, " + << index << "\n"; + index = neg; + } + const Type element = instruction.type.pointee ? *instruction.type.pointee + : Type::builtin("u8"); + os << " " << instruction.result << " = getelementptr " + << llvm_type(element) << ", ptr " << value_of(instruction.args[0]) + << ", " << llvm_type(index_type) << ' ' << index << "\n"; + } else if (instruction.text == "!" || instruction.text == "~") { + os << " " << instruction.result << " = xor " << llvm_type(instruction.type) + << ' ' << value_of(instruction.args[0]) << ", " + << (instruction.type.kind == TypeKind::Bool ? "true" : "-1") << "\n"; + } else { + os << " " << instruction.result << " = " + << arithmetic_opcode(instruction.type, instruction.text) << ' ' + << llvm_type(instruction.type) << ' ' << value_of(instruction.args[0]) + << ", " << value_of(instruction.args[1]) << "\n"; + } + values[instruction.result] = instruction.result; + value_types[instruction.result] = instruction.type; + break; + } + case mir::Op::Compare: + os << " " << instruction.result << " = " + << (instruction.type.is_float() ? "fcmp " : "icmp ") + << compare_predicate(instruction.type, instruction.text) << ' ' + << llvm_type(instruction.type) << ' ' << value_of(instruction.args[0]) + << ", " << value_of(instruction.args[1]) << "\n"; + values[instruction.result] = instruction.result; + value_types[instruction.result] = Type::builtin("bool"); + break; + case mir::Op::Call: { + if (auto arity = syscall_arity_from_name(instruction.text); + arity && instruction.args.size() == static_cast(*arity) + 1) { + if (emit_syscall(os, instruction, values, value_types, asm_counter)) + break; + } + const auto fit = functions.find(instruction.text); + std::vector params; + if (fit != functions.end()) + for (const auto& param : fit->second->params) + params.push_back(param.type); + + if ((instruction.text == "uinx_volatile_load_u8" || + instruction.text == "uinx_volatile_load_u32" || + instruction.text == "uinx_volatile_load_u64") && + instruction.args.size() == 1) { + os << " " << instruction.result << " = load volatile " + << llvm_type(instruction.type) << ", ptr " + << value_of(instruction.args[0]) << ", align " + << type_alignment(instruction.type) << "\n"; + values[instruction.result] = instruction.result; + value_types[instruction.result] = instruction.type; + break; + } + if ((instruction.text == "uinx_volatile_store_u8" || + instruction.text == "uinx_volatile_store_u32" || + instruction.text == "uinx_volatile_store_u64") && + instruction.args.size() == 2) { + const Type value_type = + params.size() > 1 ? params[1] : Type::builtin("u64"); + os << " store volatile " << llvm_type(value_type) << ' ' + << value_of(instruction.args[1]) << ", ptr " + << value_of(instruction.args[0]) << ", align " + << type_alignment(value_type) << "\n"; + break; + } + if (auto intrinsic = parse_atomic_intrinsic(instruction.text)) { + const std::string& llvm_ty = intrinsic->llvm_type; + const unsigned align = intrinsic->align; + const std::string& rmw = intrinsic->rmw_op; + if (rmw == "load" && instruction.args.size() == 2) { + if (auto order = atomic_order_from_abi(value_of(instruction.args[1])); + order && valid_atomic_load_order(*order)) { + os << " " << instruction.result << " = load atomic " << llvm_ty + << ", ptr " << value_of(instruction.args[0]) << ' ' << *order + << ", align " << align << "\n"; + values[instruction.result] = instruction.result; + value_types[instruction.result] = instruction.type; + break; + } + } + if (rmw == "store" && instruction.args.size() == 3) { + if (auto order = atomic_order_from_abi(value_of(instruction.args[2])); + order && valid_atomic_store_order(*order)) { + os << " store atomic " << llvm_ty << ' ' + << value_of(instruction.args[1]) << ", ptr " + << value_of(instruction.args[0]) << ' ' << *order << ", align " + << align << "\n"; + break; + } + } + if ((rmw == "add" || rmw == "sub" || rmw == "and" || rmw == "or" || + rmw == "xor" || rmw == "xchg") && + instruction.args.size() == 3) { + if (auto order = atomic_order_from_abi(value_of(instruction.args[2])); + order && valid_atomic_rmw_order(*order)) { + os << " " << instruction.result << " = atomicrmw " << rmw + << " ptr " << value_of(instruction.args[0]) << ", " << llvm_ty + << ' ' << value_of(instruction.args[1]) << ' ' << *order + << ", align " << align << "\n"; + values[instruction.result] = instruction.result; + value_types[instruction.result] = instruction.type; + break; + } + } + if (rmw == "cmpxchg" && instruction.args.size() == 5) { + auto success = atomic_order_from_abi(value_of(instruction.args[3])); + auto failure = atomic_order_from_abi(value_of(instruction.args[4])); + if (success && failure && valid_atomic_rmw_order(*success) && + (valid_atomic_load_order(*failure) || *failure == "monotonic")) { + const std::string failure_fixed = + downgrade_cmpxchg_failure(*success, *failure); + const std::string id = std::to_string(atomic_counter++); + const std::string expected = "%cas.expected." + id; + const std::string pair = "%cas.pair." + id; + const std::string old = "%cas.old." + id; + const std::string ok = "%cas.ok." + id; + const std::string result = instruction.result.empty() + ? "%cas.result." + id + : instruction.result; + os << " " << expected << " = load " << llvm_ty << ", ptr " + << value_of(instruction.args[1]) << ", align " << align << "\n"; + os << " " << pair << " = cmpxchg ptr " + << value_of(instruction.args[0]) << ", " << llvm_ty << ' ' + << expected << ", " << llvm_ty << ' ' + << value_of(instruction.args[2]) << ' ' << *success << ' ' + << failure_fixed << ", align " << align << "\n"; + os << " " << old << " = extractvalue { " << llvm_ty << ", i1 } " + << pair << ", 0\n"; + os << " " << ok << " = extractvalue { " << llvm_ty << ", i1 } " + << pair << ", 1\n"; + os << " store " << llvm_ty << ' ' << old << ", ptr " + << value_of(instruction.args[1]) << ", align " << align << "\n"; + os << " " << result << " = zext i1 " << ok << " to i32\n"; + if (!instruction.result.empty()) { + values[instruction.result] = result; + value_types[instruction.result] = instruction.type; + } + break; + } + } + // Invalid ordering falls through to runtime call (fail-closed + // is handled by sema/codegen validation elsewhere). + } + + os << " "; + if (!instruction.result.empty()) + os << instruction.result << " = "; + os << "call " << llvm_type(instruction.type) << " @" + << llvm_name(instruction.text) << '('; + for (std::size_t i = 0; i < instruction.args.size(); ++i) { + if (i) + os << ", "; + const Type arg_type = i < params.size() + ? params[i] + : (value_types.contains(instruction.args[i]) + ? value_types[instruction.args[i]] + : Type::builtin("i32")); + os << llvm_type(arg_type) << ' ' << value_of(instruction.args[i]); + } + os << ")\n"; + if (!instruction.result.empty()) { + values[instruction.result] = instruction.result; + value_types[instruction.result] = instruction.type; + } + break; + } + case mir::Op::Cast: { + const Type from = value_types.contains(instruction.args[0]) + ? value_types[instruction.args[0]] + : Type::builtin("i32"); + if ((from.kind == TypeKind::Str && instruction.type.kind == TypeKind::RawPtr) || + ((from.kind == TypeKind::Ref || from.kind == TypeKind::RawPtr) && + (instruction.type.kind == TypeKind::Ref || + instruction.type.kind == TypeKind::RawPtr))) { + values[instruction.result] = value_of(instruction.args[0]); + value_types[instruction.result] = instruction.type; + break; + } + std::string opcode; + if (from.kind == TypeKind::RawPtr && instruction.type.is_integer()) + opcode = "ptrtoint"; + else if (from.is_integer() && instruction.type.kind == TypeKind::RawPtr) + opcode = "inttoptr"; + else if (from.is_integer() && instruction.type.is_integer()) { + const int from_bits = integer_bits(from), + to_bits = integer_bits(instruction.type); + opcode = from_bits < to_bits + ? (unsigned_integer(from.kind) ? "zext" : "sext") + : from_bits > to_bits ? "trunc" + : "bitcast"; + } else if (from.kind == TypeKind::Bool && instruction.type.is_integer()) { + opcode = "zext"; + } else if (from.is_integer() && instruction.type.is_float()) + opcode = unsigned_integer(from.kind) ? "uitofp" : "sitofp"; + else if (from.is_float() && instruction.type.is_integer()) + opcode = unsigned_integer(instruction.type.kind) ? "fptoui" : "fptosi"; + else if (from.is_float() && instruction.type.is_float()) + opcode = from.kind == TypeKind::F32 ? "fpext" : "fptrunc"; + else + opcode = "bitcast"; + os << " " << instruction.result << " = " << opcode << ' ' << llvm_type(from) + << ' ' << value_of(instruction.args[0]) << " to " + << llvm_type(instruction.type) << "\n"; + values[instruction.result] = instruction.result; + value_types[instruction.result] = instruction.type; + break; + } + case mir::Op::Await: + diags_.error(instruction.range, + "E0602", + "internal: await reached synchronous LLVM lowering"); + break; + case mir::Op::InlineAsm: + emit_inline_asm(os, instruction, values, value_types, asm_counter); + break; + case mir::Op::Drop: + os << " call void @" << llvm_name(instruction.type.name + "__drop") << "(ptr " + << value_of(instruction.args[0]) << ")\n"; + break; + case mir::Op::Jump: + os << " br label %" << llvm_name(instruction.args[0]) << "\n"; + break; + case mir::Op::CondJump: + os << " br i1 " << value_of(instruction.args[0]) << ", label %" + << llvm_name(instruction.args[1]) << ", label %" + << llvm_name(instruction.args[2]) << "\n"; + break; + case mir::Op::Return: + if (instruction.args.empty()) + os << " ret void\n"; + else + os << " ret " << llvm_type(instruction.type) << ' ' + << value_of(instruction.args[0]) << "\n"; + break; + } + } + } + os << "}\n\n"; +} +} // namespace uinx diff --git a/src/codegen_internal.hpp b/src/codegen_internal.hpp new file mode 100644 index 0000000..07bdb75 --- /dev/null +++ b/src/codegen_internal.hpp @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +// Internal helpers shared by the translation units split out of codegen.cpp. + +#pragma once + +#include "uinx/codegen.hpp" + +#include +#include +#include +#include +#include +#include + +namespace uinx::codegen_detail { + +inline bool unsigned_integer(TypeKind kind) { + return kind == TypeKind::U8 || kind == TypeKind::U16 || kind == TypeKind::U32 || + kind == TypeKind::U64 || kind == TypeKind::U128 || kind == TypeKind::Usize; +} + +inline int integer_bits(const Type& type) { + switch (type.kind) { + case TypeKind::I8: + case TypeKind::U8: + return 8; + case TypeKind::I16: + case TypeKind::U16: + return 16; + case TypeKind::I32: + case TypeKind::U32: + case TypeKind::Char: + return 32; + case TypeKind::I128: + case TypeKind::U128: + return 128; + default: + return 64; + } +} + +inline std::string llvm_integer_constant(std::string_view literal) { + bool negative = false; + std::size_t offset = 0; + if (!literal.empty() && literal.front() == '-') { + negative = true; + offset = 1; + } + + unsigned base = 10; + if (literal.size() >= offset + 2 && literal[offset] == '0') { + switch (literal[offset + 1]) { + case 'x': + case 'X': + base = 16; + offset += 2; + break; + case 'b': + case 'B': + base = 2; + offset += 2; + break; + case 'o': + case 'O': + base = 8; + offset += 2; + break; + default: + break; + } + } + + // Store the magnitude in base 1,000,000,000 so this also handles u128 + // without narrowing through a host integer type. + std::vector digits{0}; + bool has_digit = false; + for (std::size_t i = offset; i < literal.size(); ++i) { + const char c = literal[i]; + if (c == '_') + continue; + unsigned value = 0; + if (c >= '0' && c <= '9') + value = static_cast(c - '0'); + else if (c >= 'a' && c <= 'f') + value = static_cast(c - 'a') + 10; + else if (c >= 'A' && c <= 'F') + value = static_cast(c - 'A') + 10; + else + return std::string(literal); + if (value >= base) + return std::string(literal); + + has_digit = true; + std::uint64_t carry = value; + for (auto& digit : digits) { + const std::uint64_t next = static_cast(digit) * base + carry; + digit = static_cast(next % 1000000000U); + carry = next / 1000000000U; + } + if (carry != 0) + digits.push_back(static_cast(carry)); + } + if (!has_digit) + return std::string(literal); + + while (digits.size() > 1 && digits.back() == 0) + digits.pop_back(); + if (digits.size() == 1 && digits.front() == 0) + return "0"; + + std::string result = std::to_string(digits.back()); + for (std::size_t i = digits.size() - 1; i > 0; --i) { + const std::string chunk = std::to_string(digits[i - 1]); + result.append(9 - chunk.size(), '0'); + result += chunk; + } + if (negative) + result.insert(result.begin(), '-'); + return result; +} + +inline std::string arithmetic_opcode(const Type& type, std::string_view op) { + if (op == "+") + return type.is_float() ? "fadd" : "add"; + if (op == "-") + return type.is_float() ? "fsub" : "sub"; + if (op == "*") + return type.is_float() ? "fmul" : "mul"; + if (op == "/") { + if (type.is_float()) + return "fdiv"; + return unsigned_integer(type.kind) ? "udiv" : "sdiv"; + } + if (op == "%") + return unsigned_integer(type.kind) ? "urem" : "srem"; + if (op == "&&" || op == "&") + return "and"; + if (op == "||" || op == "|") + return "or"; + if (op == "^") + return "xor"; + if (op == "<<") + return "shl"; + if (op == ">>") + return unsigned_integer(type.kind) ? "lshr" : "ashr"; + return "add"; +} + +inline unsigned type_alignment(const Type& type) { + switch (type.kind) { + case TypeKind::Bool: + case TypeKind::I8: + case TypeKind::U8: + return 1; + case TypeKind::I16: + case TypeKind::U16: + return 2; + case TypeKind::I32: + case TypeKind::U32: + case TypeKind::Char: + case TypeKind::F32: + return 4; + case TypeKind::I128: + case TypeKind::U128: + return 16; + default: + return 8; + } +} + +inline std::string atomic_rmw_opcode(std::string_view op) { + if (op == "+") + return "add"; + if (op == "-") + return "sub"; + if (op == "&") + return "and"; + if (op == "|") + return "or"; + if (op == "^") + return "xor"; + return "xchg"; +} + +inline std::optional atomic_order_from_abi(std::string_view value) { + if (value == "0") + return "monotonic"; + if (value == "1") + return "acquire"; + if (value == "2") + return "release"; + if (value == "3") + return "acq_rel"; + if (value == "4") + return "seq_cst"; + return std::nullopt; +} + +inline std::string cmpxchg_failure_order(std::string order) { + if (order == "release") + return "monotonic"; + if (order == "acq_rel") + return "acquire"; + return order; +} + +// Industrial C++20 ordering validation (Rust + C++20 + LKMM fusion). +// load: relaxed/acquire/seq_cst; store: relaxed/release/seq_cst; +// rmw/exchange: all five; cmpxchg failure: never release/acq_rel. +inline bool valid_atomic_load_order(std::string_view order) { + return order == "monotonic" || order == "acquire" || order == "seq_cst"; +} + +inline bool valid_atomic_store_order(std::string_view order) { + return order == "monotonic" || order == "release" || order == "seq_cst"; +} + +inline bool valid_atomic_rmw_order(std::string_view order) { + return order == "monotonic" || order == "acquire" || order == "release" || order == "acq_rel" || + order == "seq_cst"; +} + +inline int order_strength(std::string_view order) { + if (order == "monotonic") + return 0; + if (order == "acquire" || order == "release") + return 1; + if (order == "acq_rel") + return 2; + if (order == "seq_cst") + return 3; + return -1; +} + +inline std::string downgrade_cmpxchg_failure(std::string success, std::string failure) { + failure = cmpxchg_failure_order(std::move(failure)); + // Failure ordering must not exceed success ordering. + if (order_strength(failure) > order_strength(success)) { + if (success == "monotonic" || success == "release") + return "monotonic"; + return "acquire"; + } + return failure; +} + +struct AtomicIntrinsic { + std::string llvm_type; + unsigned align; + std::string rmw_op; // add/sub/and/or/xor/xchg/cmpxchg/load/store +}; + +inline std::optional parse_atomic_intrinsic(std::string_view name) { + // uinx_atomic_{load,store,fetch_add,fetch_sub,fetch_and,fetch_or,fetch_xor, + // exchange,compare_exchange}_{u8,u16,u32,u64} + static constexpr std::string_view prefix = "uinx_atomic_"; + if (!name.starts_with(prefix)) + return std::nullopt; + std::string_view rest = name.substr(prefix.size()); + std::string op; + for (std::string_view candidate : {"compare_exchange", + "fetch_add", + "fetch_sub", + "fetch_and", + "fetch_or", + "fetch_xor", + "exchange", + "load", + "store"}) { + if (rest.starts_with(candidate)) { + op = std::string(candidate); + rest = rest.substr(candidate.size()); + break; + } + } + if (op.empty() || !rest.starts_with("_")) + return std::nullopt; + std::string_view width = rest.substr(1); + std::string llvm_type; + unsigned align = 1; + if (width == "u8") { + llvm_type = "i8"; + align = 1; + } else if (width == "u16") { + llvm_type = "i16"; + align = 2; + } else if (width == "u32") { + llvm_type = "i32"; + align = 4; + } else if (width == "u64") { + llvm_type = "i64"; + align = 8; + } else { + return std::nullopt; + } + std::string rmw; + if (op == "load") + rmw = "load"; + else if (op == "store") + rmw = "store"; + else if (op == "fetch_add") + rmw = "add"; + else if (op == "fetch_sub") + rmw = "sub"; + else if (op == "fetch_and") + rmw = "and"; + else if (op == "fetch_or") + rmw = "or"; + else if (op == "fetch_xor") + rmw = "xor"; + else if (op == "exchange") + rmw = "xchg"; + else if (op == "compare_exchange") + rmw = "cmpxchg"; + return AtomicIntrinsic{llvm_type, align, rmw}; +} + +// Number of arguments encoded in a uinx_syscallN intrinsic name, or +// std::nullopt when the symbol is an ordinary runtime call. +inline std::optional syscall_arity_from_name(std::string_view name) { + static constexpr std::string_view prefix = "uinx_syscall"; + if (!name.starts_with(prefix)) + return std::nullopt; + const std::string_view suffix = name.substr(prefix.size()); + if (suffix.empty() || suffix.size() > 1) + return std::nullopt; + if (suffix.front() < '0' || suffix.front() > '6') + return std::nullopt; + return suffix.front() - '0'; +} + +inline std::string compare_predicate(const Type& type, std::string_view op) { + if (type.is_float()) { + if (op == "==") + return "oeq"; + if (op == "!=") + return "une"; + if (op == "<") + return "olt"; + if (op == "<=") + return "ole"; + if (op == ">") + return "ogt"; + return "oge"; + } + const bool u = unsigned_integer(type.kind); + if (op == "==") + return "eq"; + if (op == "!=") + return "ne"; + if (op == "<") + return u ? "ult" : "slt"; + if (op == "<=") + return u ? "ule" : "sle"; + if (op == ">") + return u ? "ugt" : "sgt"; + return u ? "uge" : "sge"; +} +} // namespace uinx::codegen_detail diff --git a/src/sema.cpp b/src/sema.cpp index 5df0701..d3128a1 100644 --- a/src/sema.cpp +++ b/src/sema.cpp @@ -4,438 +4,15 @@ #include "uinx/sema.hpp" +#include "sema_internal.hpp" + #include #include #include namespace uinx { -namespace { -Type replace_self(Type type, const Type& owner) { - if (type.kind == TypeKind::Named && type.name == "Self") - return owner; - if (type.pointee) - type.pointee = std::make_shared(replace_self(*type.pointee, owner)); - for (auto& arg : type.args) - arg = replace_self(std::move(arg), owner); - return type; -} - -bool integer_literal(const ast::Expr* expr) { - if (!expr) - return false; - if (expr->kind == ast::ExprKind::Integer) - return true; - if (expr->kind == ast::ExprKind::Unary) { - const auto& unary = static_cast(*expr); - return unary.op == "-" && unary.operand && unary.operand->kind == ast::ExprKind::Integer; - } - return false; -} - -bool can_coerce_expr(const ast::Expr* expr, const Type& from, const Type& to) { - if (can_coerce(from, to)) - return true; - if (expr && expr->kind == ast::ExprKind::String && from.kind == TypeKind::Ref && from.pointee && - from.pointee->kind == TypeKind::Str && to.kind == TypeKind::Str) - return true; - return integer_literal(expr) && from.is_integer() && to.is_integer(); -} - -bool static_initializer_expr(const ast::Expr& expr) { - if (expr.kind == ast::ExprKind::Integer || expr.kind == ast::ExprKind::Float || - expr.kind == ast::ExprKind::Bool || expr.kind == ast::ExprKind::Char) - return true; - if (expr.kind == ast::ExprKind::StructLiteral) { - const auto& structure = static_cast(expr); - return std::all_of( - structure.fields.begin(), structure.fields.end(), [](const ast::FieldInit& field) { - if (!field.value) - return false; - if (field.value->kind == ast::ExprKind::Integer) - return static_cast(*field.value).value == "0"; - if (field.value->kind == ast::ExprKind::Bool) - return !static_cast(*field.value).value; - return false; - }); - } - return false; -} - -bool builtin_function(std::string_view name) { - static const std::unordered_set names{ - "print", "println", "eprint", "eprintln", "input", "str", "int", "float", "bool", "len"}; - return names.contains(name); -} -} // namespace -hir::SymbolId NameResolver::add(hir::Module& out, - hir::SymbolKind kind, - std::string name, - SourceRange range, - std::optional parent) { - hir::SymbolId id = static_cast(out.symbols.size()); - out.symbols.push_back({id, kind, std::move(name), std::move(range), parent}); - return id; -} -hir::Module NameResolver::resolve(const ast::Module& module) { - hir::Module out; - out.ast = &module; - for (const auto& item : module.items) { - std::visit( - [&](const auto& x) { - using T = std::decay_t; - if constexpr (std::is_same_v || - std::is_same_v || - std::is_same_v || - std::is_same_v) { - hir::SymbolKind k = hir::SymbolKind::Function; - if constexpr (std::is_same_v) - k = hir::SymbolKind::Struct; - else if constexpr (std::is_same_v) - k = hir::SymbolKind::Trait; - else if constexpr (std::is_same_v) - k = hir::SymbolKind::Global; - if (out.globals.contains(x.name)) { - if constexpr (std::is_same_v) { - if (!x.is_extern) - diags_.error( - x.range, "E0200", "duplicate global symbol '" + x.name + "'"); - } else - diags_.error( - x.range, "E0200", "duplicate global symbol '" + x.name + "'"); - } else - out.globals[x.name] = add(out, k, x.name, x.range); - } - }, - item); - } - for (const auto& item : module.items) - if (auto f = std::get_if(&item)) - resolve_function(out, *f); - else if (auto im = std::get_if(&item)) - for (const auto& m : im->methods) - resolve_function(out, m); - return out; -} -void NameResolver::resolve_function(hir::Module& out, const ast::FunctionDecl& fn) { - std::optional owner; - auto gi = out.globals.find(fn.name); - if (gi != out.globals.end()) - owner = gi->second; - std::vector> scopes(1); - for (const auto& g : fn.generics) { - auto id = add(out, hir::SymbolKind::Generic, g.name, g.range, owner); - if (scopes.back().contains(g.name)) - diags_.error(g.range, "E0201", "duplicate generic parameter '" + g.name + "'"); - else - scopes.back()[g.name] = id; - } - for (const auto& p : fn.params) { - auto id = add(out, hir::SymbolKind::Parameter, p.name, p.range, owner); - if (scopes.back().contains(p.name)) - diags_.error(p.range, "E0202", "duplicate parameter '" + p.name + "'"); - else - scopes.back()[p.name] = id; - out.params[&p] = id; - } - if (fn.body) - resolve_block(out, *fn.body, scopes, owner); -} -void NameResolver::resolve_block( - hir::Module& out, - const ast::BlockStmt& block, - std::vector>& scopes, - std::optional owner) { - scopes.emplace_back(); - for (const auto& sp : block.stmts) { - const auto& s = *sp; - if (auto l = dynamic_cast(&s)) { - if (l->init) - resolve_expr(out, *l->init, scopes); - auto id = add(out, hir::SymbolKind::Local, l->name, l->range, owner); - if (scopes.back().contains(l->name)) - diags_.error( - l->range, "E0203", "binding '" + l->name + "' already exists in this scope"); - else - scopes.back()[l->name] = id; - out.bindings[l] = id; - } else if (auto a = dynamic_cast(&s)) { - if (a->op == "=" && a->target->kind == ast::ExprKind::Name) { - const auto& name = static_cast(*a->target).name; - bool resolved = out.globals.contains(name); - for (auto it = scopes.rbegin(); !resolved && it != scopes.rend(); ++it) - resolved = it->contains(name); - if (!resolved) { - resolve_expr(out, *a->value, scopes); - auto id = add(out, hir::SymbolKind::Local, name, a->target->range, owner); - scopes.back()[name] = id; - out.assignment_bindings[a] = id; - out.expr_resolution[a->target.get()] = id; - continue; - } - } - resolve_expr(out, *a->target, scopes); - resolve_expr(out, *a->value, scopes); - } else if (auto e = dynamic_cast(&s)) { - resolve_expr(out, *e->expr, scopes); - } else if (auto r = dynamic_cast(&s)) { - if (r->value) - resolve_expr(out, *r->value, scopes); - } else if (auto b = dynamic_cast(&s)) { - resolve_block(out, *b, scopes, owner); - } else if (auto i = dynamic_cast(&s)) { - resolve_expr(out, *i->condition, scopes); - resolve_block(out, *i->then_block, scopes, owner); - if (i->else_block) - resolve_block(out, *i->else_block, scopes, owner); - } else if (auto w = dynamic_cast(&s)) { - resolve_expr(out, *w->condition, scopes); - resolve_block(out, *w->body, scopes, owner); - } else if (auto f = dynamic_cast(&s)) { - resolve_expr(out, *f->begin, scopes); - resolve_expr(out, *f->end, scopes); - scopes.emplace_back(); - auto id = add(out, hir::SymbolKind::Local, f->name, f->range, owner); - scopes.back()[f->name] = id; - out.for_bindings[f] = id; - resolve_block(out, *f->body, scopes, owner); - scopes.pop_back(); - } else if (auto loop = dynamic_cast(&s)) { - resolve_block(out, *loop->body, scopes, owner); - } else if (auto u = dynamic_cast(&s)) { - resolve_block(out, *u->body, scopes, owner); - } - } - scopes.pop_back(); -} -void NameResolver::resolve_expr( - hir::Module& out, - const ast::Expr& e, - std::vector>& scopes) { - switch (e.kind) { - case ast::ExprKind::Name: { - auto& n = static_cast(e); - for (auto it = scopes.rbegin(); it != scopes.rend(); ++it) { - auto f = it->find(n.name); - if (f != it->end()) { - out.expr_resolution[&e] = f->second; - return; - } - } - auto g = out.globals.find(n.name); - if (g != out.globals.end()) { - out.expr_resolution[&e] = g->second; - return; - } - if (builtin_function(n.name)) - return; - diags_.error(e.range, "E0204", "unresolved name '" + n.name + "'"); - break; - } - case ast::ExprKind::StructLiteral: { - auto& st = static_cast(e); - auto g = out.globals.find(st.name); - if (g == out.globals.end()) - diags_.error(e.range, "E0206", "unknown struct '" + st.name + "'"); - else - out.expr_resolution[&e] = g->second; - for (auto& f : st.fields) - resolve_expr(out, *f.value, scopes); - break; - } - case ast::ExprKind::Unary: - resolve_expr(out, *static_cast(e).operand, scopes); - break; - case ast::ExprKind::Binary: { - auto& b = static_cast(e); - resolve_expr(out, *b.lhs, scopes); - resolve_expr(out, *b.rhs, scopes); - break; - } - case ast::ExprKind::Borrow: - resolve_expr(out, *static_cast(e).target, scopes); - break; - case ast::ExprKind::Call: { - auto& c = static_cast(e); - resolve_expr(out, *c.callee, scopes); - for (auto& a : c.args) - resolve_expr(out, *a, scopes); - break; - } - case ast::ExprKind::Member: - resolve_expr(out, *static_cast(e).base, scopes); - break; - case ast::ExprKind::Index: { - auto& i = static_cast(e); - resolve_expr(out, *i.base, scopes); - resolve_expr(out, *i.index, scopes); - break; - } - case ast::ExprKind::Await: - resolve_expr(out, *static_cast(e).value, scopes); - break; - case ast::ExprKind::Cast: - resolve_expr(out, *static_cast(e).value, scopes); - break; - case ast::ExprKind::Asm: { - auto& a = static_cast(e); - for (auto& o : a.operands) - if (o.value) - resolve_expr(out, *o.value, scopes); - for (auto& o : a.operands) - if (!o.out_name.empty()) { - bool found = false; - for (auto it = scopes.rbegin(); it != scopes.rend(); ++it) - if (auto f = it->find(o.out_name); f != it->end()) { - found = true; - break; - } - if (!found) - diags_.error( - o.range, "E0205", "unknown asm output binding '" + o.out_name + "'"); - } - break; - } - default: - break; - } -} - -bool TraitSolver::satisfies(const Type& type, std::string_view trait) const { - return satisfies(type, trait, {}); -} - -bool TraitSolver::satisfies( - const Type& type, - std::string_view trait, - const std::unordered_map>& generic_bounds) const { - if (trait == "Copy") - return type.is_copy(); - if (trait != "Send" && trait != "Sync") - return false; - - std::function&)> - unify_pattern; - unify_pattern = [&](const Type& pattern, - const Type& actual, - std::unordered_map& substitutions) -> bool { - if (pattern.kind == TypeKind::Generic) { - auto [it, inserted] = substitutions.emplace(pattern.name, actual); - return inserted || it->second == actual; - } - if (pattern.kind != actual.kind || pattern.name != actual.name || - pattern.mut != actual.mut || pattern.args.size() != actual.args.size()) - return false; - if (static_cast(pattern.pointee) != static_cast(actual.pointee)) - return false; - if (pattern.pointee && !unify_pattern(*pattern.pointee, *actual.pointee, substitutions)) - return false; - for (std::size_t i = 0; i < pattern.args.size(); ++i) - if (!unify_pattern(pattern.args[i], actual.args[i], substitutions)) - return false; - return true; - }; +using namespace sema_detail; - std::function solve; - solve = [&](const Type& current, std::string_view current_trait, std::size_t depth) -> bool { - if (depth > 64) - return false; - - for (const auto& impl : model_.impls) - if (impl.trait == current_trait && impl.for_type == current) - return true; - - for (const auto& marker : model_.markers) { - if (marker.trait != current_trait) - continue; - std::unordered_map substitutions; - if (!unify_pattern(marker.type, current, substitutions)) - continue; - bool valid = true; - for (const auto& [generic, bounds] : marker.bounds) { - auto actual = substitutions.find(generic); - if (actual == substitutions.end()) { - valid = false; - break; - } - for (const auto& bound : bounds) { - if (!solve(actual->second, bound, depth + 1)) { - valid = false; - break; - } - } - if (!valid) - break; - } - if (valid) - return true; - } - - if (current_trait != "Send" && current_trait != "Sync") - return false; - - std::unordered_set visiting; - std::function&, - std::unordered_set)> - auto_trait; - std::unordered_set assumed; - for (const auto& [generic, bounds] : generic_bounds) - for (const auto& bound : bounds) - assumed.insert(bound + ":" + generic); - auto_trait = [&](const Type& value, - std::string_view value_trait, - std::unordered_set& active, - std::unordered_set assumptions) -> bool { - const std::string key = std::string(value_trait) + ":" + value.str(); - if (!active.insert(key).second) - return true; - const auto finish = [&](bool result) { - active.erase(key); - return result; - }; - - if (value.kind == TypeKind::RawPtr) - return finish(false); - if (value.kind == TypeKind::Ref && value.pointee) { - if (value_trait == "Send" && value.mut) - return finish( - auto_trait(*value.pointee, "Send", active, std::move(assumptions))); - return finish(auto_trait(*value.pointee, "Sync", active, std::move(assumptions))); - } - if (value.kind == TypeKind::Generic) - return finish(assumptions.contains(std::string(value_trait) + ":" + value.name)); - if (value.kind == TypeKind::Str) - return finish(true); - if (value.kind != TypeKind::Named) - return finish(value.is_copy()); - - auto structure = model_.structs.find(value.name); - if (structure == model_.structs.end()) - return finish(false); - for (const auto& [generic, bounds] : structure->second.bounds) - if (std::find(bounds.begin(), bounds.end(), value_trait) != bounds.end()) - assumptions.insert(std::string(value_trait) + ":" + generic); - - std::unordered_map substitutions; - for (std::size_t i = 0; - i < structure->second.generic_names.size() && i < value.args.size(); - ++i) - substitutions[structure->second.generic_names[i]] = value.args[i]; - for (const auto& [field_name, field_type] : structure->second.fields) { - (void)field_name; - if (!auto_trait(substitute_type(field_type, substitutions), - value_trait, - active, - assumptions)) - return finish(false); - } - return finish(true); - }; - return auto_trait(type, trait, visiting, assumed); - }; - return solve(type, trait, 0); -} void TypeChecker::collect_items(const ast::Module& module) { for (const auto& item : module.items) { if (auto f = std::get_if(&item)) { @@ -692,38 +269,6 @@ void TypeChecker::validate_special_traits() { } } -void TypeChecker::validate_concurrency_boundaries() { - TraitSolver solver(model_); - for (const auto& [key, signature] : model_.functions) { - (void)key; - if (!signature.is_concurrent || !signature.decl) - continue; - // `extern` declarations are FFI trust boundaries without bodies: the - // `unsafe` block at the Uinx call site is the audit point, and raw - // pointers are the ABI mechanism itself (e.g. `uinx_atomic_*` - // intrinsics lowered directly to LLVM atomics). Enforcing `Send` on - // their parameters would make compare-exchange/fetch-add unusable - // from exactly the concurrent paths they exist for, while checking - // nothing about an actual transfer. Uinx-level callers stay checked. - if (signature.is_extern) - continue; - for (std::size_t i = 0; i < signature.params.size(); ++i) { - const Type& parameter = signature.params[i]; - if (solver.satisfies(parameter, "Send", signature.bounds)) - continue; - - const SourceRange range = i < signature.decl->params.size() - ? signature.decl->params[i].range - : signature.decl->range; - diags_.error(range, - "E0362", - "concurrent function parameter " + std::to_string(i + 1) + " of type '" + - parameter.str() + "' is not Send; use an owning Send type, a shared " + - "reference whose pointee is Sync, or add the required generic bound"); - } - } -} - SemanticModel TypeChecker::check(const ast::Module& module) { collect_items(module); validate_special_traits(); @@ -772,1024 +317,4 @@ SemanticModel TypeChecker::check(const ast::Module& module) { validate_concurrency_boundaries(); return std::move(model_); } -void TypeChecker::propagate_concurrency(ast::SmpMode mode) { - std::unordered_map> edges; - - std::function&)> visit_expr; - std::function&)> visit_stmt; - visit_expr = [&](const ast::Expr& expr, std::unordered_set& out) { - switch (expr.kind) { - case ast::ExprKind::Unary: - visit_expr(*static_cast(expr).operand, out); - break; - case ast::ExprKind::Binary: { - const auto& b = static_cast(expr); - visit_expr(*b.lhs, out); - visit_expr(*b.rhs, out); - break; - } - case ast::ExprKind::Borrow: - visit_expr(*static_cast(expr).target, out); - break; - case ast::ExprKind::Call: { - const auto& call = static_cast(expr); - if (call.callee->kind == ast::ExprKind::Name) { - const auto& name = static_cast(*call.callee).name; - if (model_.functions.contains(name)) - out.insert(name); - } else if (call.callee->kind == ast::ExprKind::Member) { - const auto& member = static_cast(*call.callee); - auto type_it = model_.expr_types.find(member.base.get()); - if (type_it != model_.expr_types.end()) { - Type owner = type_it->second; - if (owner.kind == TypeKind::Ref && owner.pointee) - owner = *owner.pointee; - const std::string key = owner.name + "::" + member.member; - if (model_.functions.contains(key)) - out.insert(key); - } - visit_expr(*member.base, out); - } - for (const auto& arg : call.args) - visit_expr(*arg, out); - break; - } - case ast::ExprKind::Member: - visit_expr(*static_cast(expr).base, out); - break; - case ast::ExprKind::Index: { - const auto& i = static_cast(expr); - visit_expr(*i.base, out); - visit_expr(*i.index, out); - break; - } - case ast::ExprKind::Await: - visit_expr(*static_cast(expr).value, out); - break; - case ast::ExprKind::Cast: - visit_expr(*static_cast(expr).value, out); - break; - case ast::ExprKind::StructLiteral: - for (const auto& field : static_cast(expr).fields) - visit_expr(*field.value, out); - break; - case ast::ExprKind::Asm: - for (const auto& operand : static_cast(expr).operands) - if (operand.value) - visit_expr(*operand.value, out); - break; - default: - break; - } - }; - visit_stmt = [&](const ast::Stmt& stmt, std::unordered_set& out) { - if (const auto* let_stmt = dynamic_cast(&stmt)) { - if (let_stmt->init) - visit_expr(*let_stmt->init, out); - } else if (const auto* assign_stmt = dynamic_cast(&stmt)) { - visit_expr(*assign_stmt->target, out); - visit_expr(*assign_stmt->value, out); - } else if (const auto* expr_stmt = dynamic_cast(&stmt)) - visit_expr(*expr_stmt->expr, out); - else if (const auto* return_stmt = dynamic_cast(&stmt)) { - if (return_stmt->value) - visit_expr(*return_stmt->value, out); - } else if (const auto* if_stmt = dynamic_cast(&stmt)) { - visit_expr(*if_stmt->condition, out); - for (const auto& child : if_stmt->then_block->stmts) - visit_stmt(*child, out); - if (if_stmt->else_block) - for (const auto& child : if_stmt->else_block->stmts) - visit_stmt(*child, out); - } else if (const auto* while_stmt = dynamic_cast(&stmt)) { - visit_expr(*while_stmt->condition, out); - for (const auto& child : while_stmt->body->stmts) - visit_stmt(*child, out); - } else if (const auto* for_stmt = dynamic_cast(&stmt)) { - visit_expr(*for_stmt->begin, out); - visit_expr(*for_stmt->end, out); - for (const auto& child : for_stmt->body->stmts) - visit_stmt(*child, out); - } else if (const auto* loop_stmt = dynamic_cast(&stmt)) { - for (const auto& child : loop_stmt->body->stmts) - visit_stmt(*child, out); - } else if (const auto* block_stmt = dynamic_cast(&stmt)) { - for (const auto& child : block_stmt->stmts) - visit_stmt(*child, out); - } else if (const auto* unsafe_stmt = dynamic_cast(&stmt)) { - for (const auto& child : unsafe_stmt->body->stmts) - visit_stmt(*child, out); - } - }; - - for (const auto& [key, sig] : model_.functions) { - if (!sig.decl || !sig.decl->body) - continue; - auto& callees = edges[key]; - for (const auto& stmt : sig.decl->body->stmts) - visit_stmt(*stmt, callees); - } - - std::vector work; - for (const auto& [key, sig] : model_.functions) - if (sig.is_concurrent) - work.push_back(key); - std::unordered_set seen; - while (!work.empty()) { - std::string key = std::move(work.back()); - work.pop_back(); - if (!seen.insert(key).second) - continue; - auto fit = model_.functions.find(key); - if (fit != model_.functions.end()) - fit->second.is_concurrent = true; - auto edge = edges.find(key); - if (edge == edges.end()) - continue; - for (const auto& callee : edge->second) - work.push_back(callee); - } - - // All SMP modes are explicit-shared now: `auto`, `manual` and `strict` - // differ only in ordering of explicitly atomic accesses, never in - // promotion. See docs/MEMORY_MODEL.md. - (void)mode; - // Industrial-grade explicit-shared model (Rust + C++20 + LKMM fusion): - // `smp auto/manual/strict` no longer implicitly promotes mutable globals. - // Only explicitly `shared` globals/fields lower to LLVM atomics. Any - // concurrent access to a mutable non-shared, non-percpu global is E0363. - // Ordering: auto/manual => acquire/release/acq_rel for explicit atomics, - // strict => seq_cst. See docs/MEMORY_MODEL.md. - // Note: `shared bool` is rejected at definition (use AtomicBool over u8) - // because LLVM atomics must be byte-sized (i1 is not valid). - const auto atomic_compatible = [](const Type& type) { - return type.is_integer() || type.kind == TypeKind::RawPtr; - }; - - std::unordered_set reported; - std::function check_expr; - std::function check_stmt; - check_expr = [&](const ast::Expr& expr) { - if (expr.kind == ast::ExprKind::Name) { - const auto& name = static_cast(expr).name; - auto global = model_.globals.find(name); - if (global != model_.globals.end() && global->second.is_mut && - !global->second.is_percpu && !global->second.is_shared) { - const std::string key = std::to_string(expr.range.begin.line) + ":" + - std::to_string(expr.range.begin.column) + ":" + name; - if (reported.insert(key).second) { - if (atomic_compatible(global->second.type)) { - diags_.error(expr.range, - "E0363", - "concurrent access to mutable global '" + name + - "' requires explicit `shared`, a lock, or `percpu` " - "storage; smp auto no longer promotes implicitly " - "(Rust+C++20+LKMM explicit-shared model)"); - } else { - diags_.error(expr.range, - "E0363", - "concurrent access to aggregate global '" + name + - "' cannot be strengthened as one atomic object; mark " - "atomic fields shared, use a lock, or use percpu " - "storage"); - } - } - } - return; - } - if (expr.kind == ast::ExprKind::Member) { - const auto& member = static_cast(expr); - if (member.base->kind == ast::ExprKind::Name) { - const auto& base_name = static_cast(*member.base).name; - auto global = model_.globals.find(base_name); - if (global != model_.globals.end() && global->second.is_mut && - !global->second.is_percpu && global->second.type.kind == TypeKind::Named) { - auto structure = model_.structs.find(global->second.type.name); - if (structure != model_.structs.end()) { - auto field = structure->second.fields.find(member.member); - if (field != structure->second.fields.end()) { - if (structure->second.shared_fields.contains(member.member)) - return; - const std::string key = std::to_string(expr.range.begin.line) + ":" + - std::to_string(expr.range.begin.column) + ":" + - base_name + "." + member.member; - if (reported.insert(key).second) { - diags_.error( - expr.range, - "E0363", - "concurrent access to field '" + base_name + "." + - member.member + - "' requires explicit `shared` field, a lock, or " - "`percpu` storage; smp auto no longer promotes implicitly"); - } - return; - } - } - // Aggregate global itself accessed concurrently without per-field - // sharing: reject. - const std::string key = std::to_string(expr.range.begin.line) + ":" + - std::to_string(expr.range.begin.column) + ":" + - base_name; - if (reported.insert(key).second) { - diags_.error(expr.range, - "E0363", - "concurrent access to aggregate global '" + base_name + - "' cannot be strengthened as one atomic object; mark " - "atomic fields shared, use a lock, or use percpu " - "storage"); - } - return; - } - } - check_expr(*member.base); - return; - } - switch (expr.kind) { - case ast::ExprKind::Unary: - check_expr(*static_cast(expr).operand); - break; - case ast::ExprKind::Binary: { - const auto& binary = static_cast(expr); - check_expr(*binary.lhs); - check_expr(*binary.rhs); - break; - } - case ast::ExprKind::Borrow: - check_expr(*static_cast(expr).target); - break; - case ast::ExprKind::Call: { - const auto& call = static_cast(expr); - if (call.callee->kind == ast::ExprKind::Member) - check_expr(*static_cast(*call.callee).base); - for (const auto& arg : call.args) - check_expr(*arg); - break; - } - case ast::ExprKind::Index: { - const auto& index = static_cast(expr); - check_expr(*index.base); - check_expr(*index.index); - break; - } - case ast::ExprKind::Await: - check_expr(*static_cast(expr).value); - break; - case ast::ExprKind::Cast: - check_expr(*static_cast(expr).value); - break; - case ast::ExprKind::StructLiteral: - for (const auto& field : static_cast(expr).fields) - check_expr(*field.value); - break; - case ast::ExprKind::Asm: - for (const auto& operand : static_cast(expr).operands) - if (operand.value) - check_expr(*operand.value); - break; - default: - break; - } - }; - check_stmt = [&](const ast::Stmt& stmt) { - if (const auto* let_stmt = dynamic_cast(&stmt)) { - if (let_stmt->init) - check_expr(*let_stmt->init); - } else if (const auto* assign_stmt = dynamic_cast(&stmt)) { - check_expr(*assign_stmt->target); - check_expr(*assign_stmt->value); - } else if (const auto* expr_stmt = dynamic_cast(&stmt)) - check_expr(*expr_stmt->expr); - else if (const auto* return_stmt = dynamic_cast(&stmt)) { - if (return_stmt->value) - check_expr(*return_stmt->value); - } else if (const auto* if_stmt = dynamic_cast(&stmt)) { - check_expr(*if_stmt->condition); - for (const auto& child : if_stmt->then_block->stmts) - check_stmt(*child); - if (if_stmt->else_block) - for (const auto& child : if_stmt->else_block->stmts) - check_stmt(*child); - } else if (const auto* while_stmt = dynamic_cast(&stmt)) { - check_expr(*while_stmt->condition); - for (const auto& child : while_stmt->body->stmts) - check_stmt(*child); - } else if (const auto* for_stmt = dynamic_cast(&stmt)) { - check_expr(*for_stmt->begin); - check_expr(*for_stmt->end); - for (const auto& child : for_stmt->body->stmts) - check_stmt(*child); - } else if (const auto* loop_stmt = dynamic_cast(&stmt)) { - for (const auto& child : loop_stmt->body->stmts) - check_stmt(*child); - } else if (const auto* block_stmt = dynamic_cast(&stmt)) { - for (const auto& child : block_stmt->stmts) - check_stmt(*child); - } else if (const auto* unsafe_stmt = dynamic_cast(&stmt)) { - for (const auto& child : unsafe_stmt->body->stmts) - check_stmt(*child); - } - }; - - for (const auto& [key, signature] : model_.functions) { - if (!signature.is_concurrent || !signature.decl || !signature.decl->body) - continue; - for (const auto& statement : signature.decl->body->stmts) - check_stmt(*statement); - } -} - -void TypeChecker::check_function(const ast::FunctionDecl& fn, const FunctionSig& sig) { - if (!fn.body) - return; - FnContext c; - c.sig = &sig; - c.unsafe_context = fn.is_unsafe; - c.scopes.emplace_back(); - c.mutable_scopes.emplace_back(); - for (std::size_t i = 0; i < fn.params.size(); ++i) { - c.scopes.back()[fn.params[i].name] = sig.params[i]; - if (fn.params[i].mut) - c.mutable_scopes.back().insert(fn.params[i].name); - } - check_block(*fn.body, c); -} -void TypeChecker::check_block(const ast::BlockStmt& b, FnContext& c) { - c.scopes.emplace_back(); - c.mutable_scopes.emplace_back(); - for (const auto& s : b.stmts) - check_stmt(*s, c); - c.mutable_scopes.pop_back(); - c.scopes.pop_back(); -} -void TypeChecker::check_stmt(const ast::Stmt& s, FnContext& c) { - if (auto l = dynamic_cast(&s)) { - Type init = l->init ? check_expr(*l->init, c) : Type{}; - Type ty = l->type - ? type_from_ast(*l->type, - std::unordered_set(c.sig->generic_names.begin(), - c.sig->generic_names.end())) - : init; - if (l->type && !can_coerce_expr(l->init.get(), init, ty)) - diags_.error(l->range, - "E0301", - "initializer has type '" + init.str() + "' but binding expects '" + - ty.str() + "'"); - model_.binding_types[l] = ty; - c.scopes.back()[l->name] = ty; - if (l->mut) - c.mutable_scopes.back().insert(l->name); - return; - } - if (auto a = dynamic_cast(&s)) { - if (auto binding = model_.hir.assignment_bindings.find(a); - binding != model_.hir.assignment_bindings.end()) { - Type rhs = check_expr(*a->value, c); - model_.assignment_binding_types[a] = rhs; - model_.expr_types[a->target.get()] = rhs; - const auto& name = static_cast(*a->target).name; - c.scopes.back()[name] = rhs; - c.mutable_scopes.back().insert(name); - return; - } - Type lhs = check_expr(*a->target, c, false), rhs = check_expr(*a->value, c); - if (!is_mutable_place(*a->target, c)) - diags_.error(a->target->range, "E0333", "assignment requires a mutable place"); - if (!can_coerce_expr(a->value.get(), rhs, lhs)) - diags_.error( - a->range, "E0334", "cannot assign '" + rhs.str() + "' to '" + lhs.str() + "'"); - if (a->op != "=") { - const bool bitwise = a->op == "&=" || a->op == "|=" || a->op == "^=" || - a->op == "<<=" || a->op == ">>=" || a->op == "%="; - if (bitwise ? (!lhs.is_integer() || !rhs.is_integer()) - : (!lhs.is_numeric() || !rhs.is_numeric())) - diags_.error(a->range, - "E0335", - "compound assignment operator '" + a->op + - "' is not valid for these operands"); - } - return; - } - if (auto e = dynamic_cast(&s)) { - check_expr(*e->expr, c); - return; - } - if (auto r = dynamic_cast(&s)) { - Type got = r->value ? check_expr(*r->value, c) : Type::builtin("unit"); - if (!can_coerce_expr(r->value.get(), got, c.sig->result)) - diags_.error(r->range, - "E0302", - "return type '" + got.str() + "' does not match '" + c.sig->result.str() + - "'"); - return; - } - if (auto b = dynamic_cast(&s)) { - check_block(*b, c); - return; - } - if (auto i = dynamic_cast(&s)) { - Type t = check_expr(*i->condition, c); - if (t.kind != TypeKind::Bool) - diags_.error( - i->condition->range, "E0303", "if condition must be bool, found '" + t.str() + "'"); - check_block(*i->then_block, c); - if (i->else_block) - check_block(*i->else_block, c); - return; - } - if (auto w = dynamic_cast(&s)) { - Type t = check_expr(*w->condition, c); - if (t.kind != TypeKind::Bool) - diags_.error(w->condition->range, "E0304", "while condition must be bool"); - ++c.loop_depth; - check_block(*w->body, c); - --c.loop_depth; - return; - } - if (auto f = dynamic_cast(&s)) { - Type begin = check_expr(*f->begin, c), end = check_expr(*f->end, c); - if (!begin.is_integer() || !end.is_integer() || begin != end) - diags_.error(f->range, "E0356", "for ranges require matching integer endpoint types"); - c.scopes.emplace_back(); - c.mutable_scopes.emplace_back(); - c.scopes.back()[f->name] = begin; - ++c.loop_depth; - check_block(*f->body, c); - --c.loop_depth; - c.mutable_scopes.pop_back(); - c.scopes.pop_back(); - return; - } - if (auto l = dynamic_cast(&s)) { - ++c.loop_depth; - check_block(*l->body, c); - --c.loop_depth; - return; - } - if (dynamic_cast(&s) || dynamic_cast(&s)) { - if (c.loop_depth == 0) - diags_.error(s.range, "E0357", "break/continue is only valid inside a loop"); - return; - } - if (auto u = dynamic_cast(&s)) { - bool old = c.unsafe_context; - c.unsafe_context = true; - check_block(*u->body, c); - c.unsafe_context = old; - return; - } -} -Type TypeChecker::lookup_local(std::string_view n, const FnContext& c, const SourceRange& r) { - for (auto it = c.scopes.rbegin(); it != c.scopes.rend(); ++it) { - auto f = it->find(std::string(n)); - if (f != it->end()) - return f->second; - } - auto g = model_.globals.find(std::string(n)); - if (g != model_.globals.end()) - return g->second.type; - diags_.error(r, "E0305", "name '" + std::string(n) + "' is not a value in this scope"); - return {}; -} -bool TypeChecker::is_mutable_binding(std::string_view n, const FnContext& c) const { - for (auto it = c.mutable_scopes.rbegin(); it != c.mutable_scopes.rend(); ++it) - if (it->contains(std::string(n))) - return true; - auto g = model_.globals.find(std::string(n)); - return g != model_.globals.end() && g->second.is_mut && !g->second.is_const; -} -bool TypeChecker::is_mutable_place(const ast::Expr& e, const FnContext& c) { - if (e.kind == ast::ExprKind::Name) - return is_mutable_binding(static_cast(e).name, c); - if (e.kind == ast::ExprKind::Member) { - const auto& m = static_cast(e); - Type base{}; - if (auto it = model_.expr_types.find(m.base.get()); it != model_.expr_types.end()) - base = it->second; - if (base.kind == TypeKind::Ref) - return base.mut; - return is_mutable_place(*m.base, c); - } - if (e.kind == ast::ExprKind::Index) { - const auto& i = static_cast(e); - Type base{}; - if (auto it = model_.expr_types.find(i.base.get()); it != model_.expr_types.end()) - base = it->second; - if (base.kind == TypeKind::Ref) - return base.mut; - if (base.kind == TypeKind::Named && base.name == "SliceMut") - return is_mutable_place(*i.base, c); - return false; - } - if (e.kind == ast::ExprKind::Unary) { - const auto& u = static_cast(e); - if (u.op == "*") { - Type p{}; - if (auto it = model_.expr_types.find(u.operand.get()); it != model_.expr_types.end()) - p = it->second; - return p.kind == TypeKind::Ref ? p.mut : (p.kind == TypeKind::RawPtr ? p.mut : false); - } - } - return false; -} -bool TypeChecker::unify_generic(const Type& f, - const Type& a, - std::unordered_map& s) { - if (f.kind == TypeKind::Generic) { - auto it = s.find(f.name); - if (it == s.end()) { - s[f.name] = a; - return true; - } - return it->second == a; - } - if (f.kind != a.kind) - return false; - if (f.pointee && a.pointee) - return f.mut == a.mut && unify_generic(*f.pointee, *a.pointee, s); - if (f.args.size() != a.args.size()) - return false; - for (std::size_t i = 0; i < f.args.size(); ++i) - if (!unify_generic(f.args[i], a.args[i], s)) - return false; - return f.name == a.name || f.name.empty() || a.name.empty(); -} -Type TypeChecker::check_call(const ast::CallExpr& call, FnContext& c) { - const FunctionSig* sig = nullptr; - std::string display_name; - std::vector> actuals; - - if (call.callee->kind == ast::ExprKind::Name) { - const auto& n = static_cast(*call.callee); - if (builtin_function(n.name)) - return check_builtin_call(call, c); - auto it = model_.functions.find(n.name); - if (it == model_.functions.end()) { - diags_.error(call.range, "E0307", "unknown function '" + n.name + "'"); - return {}; - } - sig = &it->second; - display_name = n.name; - for (auto& arg : call.args) - actuals.push_back({arg.get(), check_expr(*arg, c)}); - } else if (call.callee->kind == ast::ExprKind::Member) { - const auto& member = static_cast(*call.callee); - Type receiver = check_expr(*member.base, c, false); - Type owner = receiver; - if (owner.kind == TypeKind::Ref && owner.pointee) - owner = *owner.pointee; - if (owner.kind != TypeKind::Named) { - diags_.error(call.range, "E0306", "method receiver must be a named type"); - return {}; - } - const std::string key = owner.name + "::" + member.member; - auto it = model_.functions.find(key); - if (it == model_.functions.end()) { - diags_.error(call.range, - "E0307", - "type '" + owner.str() + "' has no method '" + member.member + "'"); - return {}; - } - sig = &it->second; - display_name = owner.name + "." + member.member; - if (sig->params.empty()) { - diags_.error(call.range, "E0345", "method '" + display_name + "' has no self receiver"); - return sig->result; - } - Type receiver_actual = owner; - const Type& expected_receiver = sig->params.front(); - if (expected_receiver.kind == TypeKind::Ref) { - receiver_actual = Type::ref(owner, expected_receiver.mut); - if (expected_receiver.mut && !is_mutable_place(*member.base, c) && - !(receiver.kind == TypeKind::Ref && receiver.mut)) - diags_.error(member.base->range, - "E0346", - "method '" + display_name + "' requires a mutable receiver"); - } - actuals.push_back({member.base.get(), receiver_actual}); - for (auto& arg : call.args) - actuals.push_back({arg.get(), check_expr(*arg, c)}); - } else { - diags_.error(call.range, "E0306", "call target must be a function or method"); - return {}; - } - - if (sig->is_unsafe && !c.unsafe_context) - diags_.error(call.range, - "E0308", - "call to unsafe function/method '" + display_name + - "' requires unsafe context"); - if (actuals.size() != sig->params.size()) { - const std::size_t explicit_expected = - sig->params.size() - (call.callee->kind == ast::ExprKind::Member ? 1u : 0u); - diags_.error(call.range, - "E0309", - "function/method '" + display_name + "' expects " + - std::to_string(explicit_expected) + " arguments, got " + - std::to_string(call.args.size())); - return sig->result; - } - std::unordered_map subst; - if (!call.generic_args.empty()) { - if (call.generic_args.size() != sig->generic_names.size()) - diags_.error(call.range, "E0310", "wrong number of explicit generic arguments"); - else - for (std::size_t i = 0; i < call.generic_args.size(); ++i) - subst[sig->generic_names[i]] = type_from_ast(call.generic_args[i]); - } - for (std::size_t i = 0; i < actuals.size(); ++i) { - const Type actual = actuals[i].second; - const Type expected = substitute_type(sig->params[i], subst); - if (!unify_generic(sig->params[i], actual, subst) && - !can_coerce_expr(actuals[i].first, actual, expected)) - diags_.error(actuals[i].first->range, - "E0311", - "argument " + std::to_string(i + 1) + " has type '" + actual.str() + - "', expected '" + expected.str() + "'"); - } - TraitSolver solver(model_); - for (const auto& [g, bounds] : sig->bounds) { - auto si = subst.find(g); - if (si == subst.end()) { - diags_.error(call.range, "E0312", "cannot infer generic parameter '" + g + "'"); - continue; - } - for (const auto& bound : bounds) - if (!solver.satisfies(si->second, bound)) - diags_.error(call.range, - "E0313", - "type '" + si->second.str() + "' does not satisfy trait '" + bound + - "'"); - } - Type result = substitute_type(sig->result, subst); - return sig->is_async ? Type::named("Future", {result}) : result; -} - -Type TypeChecker::check_builtin_call(const ast::CallExpr& call, FnContext& c) { - const auto& name = static_cast(*call.callee).name; - const auto string_like = [](const Type& type) { - return type.kind == TypeKind::Str || - (type.kind == TypeKind::Ref && type.pointee && type.pointee->kind == TypeKind::Str); - }; - const auto printable = [&](const Type& type) { - return type.is_integer() || type.is_float() || type.kind == TypeKind::Bool || - type.kind == TypeKind::Char || string_like(type); - }; - std::vector arguments; - for (const auto& argument : call.args) - arguments.push_back(check_expr(*argument, c)); - - if (name == "print" || name == "println" || name == "eprint" || name == "eprintln") { - for (std::size_t i = 0; i < arguments.size(); ++i) - if (!printable(arguments[i])) - diags_.error(call.args[i]->range, - "E0367", - "print argument has unsupported type '" + arguments[i].str() + "'"); - return Type::builtin("unit"); - } - if (name == "input") { - if (arguments.size() > 1) - diags_.error(call.range, "E0368", "input expects zero or one prompt argument"); - if (!arguments.empty() && !string_like(arguments[0])) - diags_.error(call.args[0]->range, "E0369", "input prompt must be a string"); - return Type::ref(Type::builtin("str"), false); - } - if (name == "str") { - if (arguments.size() != 1) - diags_.error(call.range, "E0370", "str expects exactly one argument"); - else if (!printable(arguments[0])) - diags_.error(call.args[0]->range, - "E0371", - "str argument has unsupported type '" + arguments[0].str() + "'"); - return Type::ref(Type::builtin("str"), false); - } - if (name == "int" || name == "float") { - if (arguments.size() != 1) - diags_.error(call.range, "E0370", name + " expects exactly one argument"); - else if (!arguments[0].is_numeric() && !string_like(arguments[0])) - diags_.error( - call.args[0]->range, "E0372", name + " argument must be numeric or string-like"); - return Type::builtin(name == "int" ? "i64" : "f64"); - } - if (name == "bool") { - if (arguments.size() != 1) - diags_.error(call.range, "E0370", "bool expects exactly one argument"); - else if (arguments[0].kind != TypeKind::Bool && !arguments[0].is_numeric() && - !string_like(arguments[0])) - diags_.error( - call.args[0]->range, "E0373", "bool argument must be scalar or string-like"); - return Type::builtin("bool"); - } - if (name == "len") { - if (arguments.size() != 1) - diags_.error(call.range, "E0370", "len expects exactly one argument"); - else { - const Type& type = arguments[0]; - if (!string_like(type)) - diags_.error(call.args[0]->range, - "E0374", - "len is not implemented for type '" + type.str() + "'"); - } - return Type::builtin("usize"); - } - return {}; -} -void TypeChecker::validate_asm(const ast::AsmExpr& a, FnContext& c) { - if (!c.unsafe_context) - diags_.error(a.range, "E0314", "asm() is only permitted inside unsafe code"); - if (a.assembly.empty()) - diags_.warning(a.range, "W0315", "empty asm template"); - for (const auto& o : a.operands) { - if (o.constraint.empty()) - diags_.error(o.range, "E0316", "asm register/clobber constraint cannot be empty"); - for (char ch : o.constraint) - if (!(std::isalnum(static_cast(ch)) || - std::string("=+&{},~*").find(ch) != std::string::npos)) - diags_.error( - o.range, "E0317", "invalid character in asm constraint '" + o.constraint + "'"); - if (o.value) - check_expr(*o.value, c); - } -} -Type TypeChecker::check_expr(const ast::Expr& e, FnContext& c, bool) { - Type t; - switch (e.kind) { - case ast::ExprKind::Integer: - t = Type::builtin("i32"); - break; - case ast::ExprKind::Float: - t = Type::builtin("f64"); - break; - case ast::ExprKind::String: - t = Type::ref(Type::builtin("str"), false); - break; - case ast::ExprKind::Char: - t = Type::builtin("char"); - break; - case ast::ExprKind::Bool: - t = Type::builtin("bool"); - break; - case ast::ExprKind::Name: { - auto& n = static_cast(e); - auto fi = model_.functions.find(n.name); - t = fi != model_.functions.end() ? Type::named("fn") : lookup_local(n.name, c, e.range); - break; - } - case ast::ExprKind::StructLiteral: { - auto& st = static_cast(e); - auto si = model_.structs.find(st.name); - if (si == model_.structs.end()) { - diags_.error(e.range, "E0337", "unknown struct '" + st.name + "'"); - t = {}; - break; - } - std::vector type_args; - for (const auto& arg : st.generic_args) - type_args.push_back(type_from_ast(arg)); - if (type_args.size() != si->second.generic_names.size()) { - diags_.error(e.range, - "E0338", - "struct '" + st.name + "' expects " + - std::to_string(si->second.generic_names.size()) + - " type arguments, got " + std::to_string(type_args.size())); - t = {}; - break; - } - std::unordered_map subst; - for (std::size_t i = 0; i < type_args.size(); ++i) - subst[si->second.generic_names[i]] = type_args[i]; - TraitSolver solver(model_); - for (const auto& [generic, bounds] : si->second.bounds) { - const auto actual = subst.find(generic); - if (actual == subst.end()) - continue; - for (const auto& bound : bounds) - if (!solver.satisfies(actual->second, bound)) - diags_.error(e.range, - "E0362", - "type '" + actual->second.str() + - "' does not satisfy struct bound '" + generic + ": " + - bound + "'"); - } - std::unordered_set seen; - for (const auto& f : st.fields) { - if (!seen.insert(f.name).second) - diags_.error( - f.range, "E0339", "duplicate field '" + f.name + "' in struct literal"); - auto fi = si->second.fields.find(f.name); - Type actual = check_expr(*f.value, c); - if (fi == si->second.fields.end()) - diags_.error( - f.range, "E0340", "struct '" + st.name + "' has no field '" + f.name + "'"); - else { - Type expected = substitute_type(fi->second, subst); - if (!can_coerce_expr(f.value.get(), actual, expected)) - diags_.error(f.range, - "E0341", - "field '" + f.name + "' expects '" + expected.str() + - "', found '" + actual.str() + "'"); - } - } - for (const auto& [name, ft] : si->second.fields) { - (void)ft; - if (!seen.contains(name)) - diags_.error(e.range, - "E0342", - "missing field '" + name + "' in struct literal '" + st.name + - "'"); - } - t = Type::named(st.name, std::move(type_args)); - break; - } - case ast::ExprKind::Unary: { - auto& u = static_cast(e); - Type x = check_expr(*u.operand, c); - if (u.op == "!") { - if (x.kind != TypeKind::Bool && !x.is_integer()) - diags_.error(e.range, "E0318", "not/! requires bool or integer"); - t = x; - } else if (u.op == "~") { - if (!x.is_integer()) - diags_.error(e.range, "E0318", "~ requires an integer operand"); - t = x; - } else if (u.op == "-") { - if (!x.is_numeric()) - diags_.error(e.range, "E0319", "unary - requires numeric operand"); - t = x; - } else if (u.op == "move") { - t = x; - } else if (u.op == "*") { - if (x.kind != TypeKind::Ref && x.kind != TypeKind::RawPtr) { - diags_.error(e.range, "E0320", "dereference requires reference or raw pointer"); - t = {}; - } else { - if (x.kind == TypeKind::RawPtr && !c.unsafe_context) - diags_.error(e.range, - "E0321", - "dereferencing a raw pointer requires unsafe context"); - t = x.pointee ? *x.pointee : Type{}; - } - } - break; - } - case ast::ExprKind::Binary: { - auto& b = static_cast(e); - Type l = check_expr(*b.lhs, c), r = check_expr(*b.rhs, c); - if (l.is_integer() && r.is_integer() && l != r) { - if (integer_literal(b.lhs.get()) && can_coerce_expr(b.lhs.get(), l, r)) { - l = r; - model_.expr_types[b.lhs.get()] = r; - // `-literal` is a Unary node wrapping the Integer token; the - // MIR unary lowering reads the inner operand type, so keep it - // in sync or `sub i32` feeds an `i64` compare. - if (b.lhs->kind == ast::ExprKind::Unary) { - const auto& unary = static_cast(*b.lhs); - if (unary.operand) - model_.expr_types[unary.operand.get()] = r; - } - } else if (integer_literal(b.rhs.get()) && can_coerce_expr(b.rhs.get(), r, l)) { - r = l; - model_.expr_types[b.rhs.get()] = l; - if (b.rhs->kind == ast::ExprKind::Unary) { - const auto& unary = static_cast(*b.rhs); - if (unary.operand) - model_.expr_types[unary.operand.get()] = l; - } - } - } - if ((l.kind == TypeKind::RawPtr || r.kind == TypeKind::RawPtr) && - (b.op == "+" || b.op == "-") && !c.unsafe_context) - diags_.error(e.range, "E0322", "raw pointer arithmetic requires unsafe context"); - if (b.op == "==" || b.op == "!=" || b.op == "<" || b.op == "<=" || b.op == ">" || - b.op == ">=") { - if (!can_coerce(l, r) && !can_coerce(r, l)) - diags_.error(e.range, "E0323", "comparison operands have incompatible types"); - t = Type::builtin("bool"); - } else if (b.op == "&&" || b.op == "||") { - if (l.kind != TypeKind::Bool || r.kind != TypeKind::Bool) - diags_.error(e.range, "E0324", "logical operators require bool operands"); - t = Type::builtin("bool"); - } else if (b.op == "&" || b.op == "|" || b.op == "^" || b.op == "<<" || b.op == ">>") { - if (l != r || !l.is_integer()) - diags_.error(e.range, - "E0325", - "bitwise/shift operators require matching integer operands"); - t = l; - } else if (l.kind == TypeKind::RawPtr && (b.op == "+" || b.op == "-") && r.is_integer()) - t = l; - else if (l == r && l.is_numeric()) - t = l; - else { - diags_.error(e.range, - "E0325", - "binary operator '" + b.op + "' cannot combine '" + l.str() + - "' and '" + r.str() + "'"); - t = {}; - } - break; - } - case ast::ExprKind::Borrow: { - auto& b = static_cast(e); - Type target = check_expr(*b.target, c, false); - if (b.mut && !is_mutable_place(*b.target, c)) - diags_.error(e.range, "E0336", "mutable borrow requires a mutable place"); - t = Type::ref(target, b.mut); - break; - } - case ast::ExprKind::Call: - t = check_call(static_cast(e), c); - break; - case ast::ExprKind::Member: { - auto& m = static_cast(e); - Type base = check_expr(*m.base, c, false); - if (base.kind == TypeKind::Ref && base.pointee) - base = *base.pointee; - if (base.kind != TypeKind::Named || !model_.structs.contains(base.name)) { - diags_.error(e.range, "E0326", "member access requires struct type"); - t = {}; - } else { - auto& si = model_.structs.at(base.name); - auto f = si.fields.find(m.member); - if (f == si.fields.end()) { - diags_.error(e.range, - "E0327", - "struct '" + base.name + "' has no field '" + m.member + "'"); - t = {}; - } else { - std::unordered_map subst; - for (std::size_t gi = 0; gi < si.generic_names.size() && gi < base.args.size(); - ++gi) - subst[si.generic_names[gi]] = base.args[gi]; - t = substitute_type(f->second, subst); - } - } - break; - } - case ast::ExprKind::Index: { - auto& i = static_cast(e); - Type b = check_expr(*i.base, c, false), idx = check_expr(*i.index, c); - if (!idx.is_integer()) - diags_.error(i.index->range, "E0328", "index must be integer"); - Type container = b; - if (container.kind == TypeKind::Ref && container.pointee) - container = *container.pointee; - if (container.kind == TypeKind::Named && - (container.name == "Slice" || container.name == "SliceMut") && - !container.args.empty()) - t = container.args[0]; - else if (container.kind == TypeKind::Str) - t = Type::builtin("u8"); - else { - diags_.error(e.range, "E0329", "type '" + b.str() + "' is not safely indexable"); - t = {}; - } - break; - } - case ast::ExprKind::Await: { - if (!c.sig->is_async) - diags_.error(e.range, "E0343", "await is only valid inside an async function"); - Type f = check_expr(*static_cast(e).value, c); - if (f.kind != TypeKind::Named || f.name != "Future" || f.args.size() != 1) { - diags_.error(e.range, "E0330", "await requires Future"); - t = {}; - } else - t = f.args[0]; - break; - } - case ast::ExprKind::Cast: { - auto& x = static_cast(e); - Type from = check_expr(*x.value, c), to = type_from_ast(x.to); - bool ref_to_raw = from.kind == TypeKind::Ref && to.kind == TypeKind::RawPtr && - from.pointee && to.pointee; - if (ref_to_raw) { - if (!can_coerce(*from.pointee, *to.pointee)) - diags_.error( - e.range, "E0332", "reference/raw-pointer pointee types are incompatible"); - if (to.mut && !from.mut) - diags_.error(e.range, - "E0332", - "cannot create mutable raw pointer from shared reference"); - } else if ((from.kind == TypeKind::RawPtr || to.kind == TypeKind::RawPtr) && - !(from.kind == TypeKind::Str && to.kind == TypeKind::RawPtr) && - !c.unsafe_context) - diags_.error(e.range, "E0331", "this raw pointer cast requires unsafe context"); - const bool str_to_raw = from.kind == TypeKind::Str && to.kind == TypeKind::RawPtr && - to.pointee && to.pointee->kind == TypeKind::U8; - if (!(from.is_numeric() && to.is_numeric()) && !ref_to_raw && !str_to_raw && - from.kind != TypeKind::RawPtr && to.kind != TypeKind::RawPtr) - diags_.error(e.range, - "E0332", - "unsupported cast from '" + from.str() + "' to '" + to.str() + "'"); - t = to; - break; - } - case ast::ExprKind::Asm: { - auto& a = static_cast(e); - validate_asm(a, c); - t = Type::builtin("unit"); - break; - } - } - model_.expr_types[&e] = t; - return t; -} } // namespace uinx diff --git a/src/sema_expr.cpp b/src/sema_expr.cpp new file mode 100644 index 0000000..47c06bd --- /dev/null +++ b/src/sema_expr.cpp @@ -0,0 +1,494 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "sema_internal.hpp" +#include "uinx/sema.hpp" + +#include +#include +#include + +namespace uinx { +using namespace sema_detail; + +Type TypeChecker::check_call(const ast::CallExpr& call, FnContext& c) { + const FunctionSig* sig = nullptr; + std::string display_name; + std::vector> actuals; + + if (call.callee->kind == ast::ExprKind::Name) { + const auto& n = static_cast(*call.callee); + if (builtin_function(n.name)) + return check_builtin_call(call, c); + auto it = model_.functions.find(n.name); + if (it == model_.functions.end()) { + diags_.error(call.range, "E0307", "unknown function '" + n.name + "'"); + return {}; + } + sig = &it->second; + display_name = n.name; + for (auto& arg : call.args) + actuals.push_back({arg.get(), check_expr(*arg, c)}); + } else if (call.callee->kind == ast::ExprKind::Member) { + const auto& member = static_cast(*call.callee); + Type receiver = check_expr(*member.base, c, false); + Type owner = receiver; + if (owner.kind == TypeKind::Ref && owner.pointee) + owner = *owner.pointee; + if (owner.kind != TypeKind::Named) { + diags_.error(call.range, "E0306", "method receiver must be a named type"); + return {}; + } + const std::string key = owner.name + "::" + member.member; + auto it = model_.functions.find(key); + if (it == model_.functions.end()) { + diags_.error(call.range, + "E0307", + "type '" + owner.str() + "' has no method '" + member.member + "'"); + return {}; + } + sig = &it->second; + display_name = owner.name + "." + member.member; + if (sig->params.empty()) { + diags_.error(call.range, "E0345", "method '" + display_name + "' has no self receiver"); + return sig->result; + } + Type receiver_actual = owner; + const Type& expected_receiver = sig->params.front(); + if (expected_receiver.kind == TypeKind::Ref) { + receiver_actual = Type::ref(owner, expected_receiver.mut); + if (expected_receiver.mut && !is_mutable_place(*member.base, c) && + !(receiver.kind == TypeKind::Ref && receiver.mut)) + diags_.error(member.base->range, + "E0346", + "method '" + display_name + "' requires a mutable receiver"); + } + actuals.push_back({member.base.get(), receiver_actual}); + for (auto& arg : call.args) + actuals.push_back({arg.get(), check_expr(*arg, c)}); + } else { + diags_.error(call.range, "E0306", "call target must be a function or method"); + return {}; + } + + if (sig->is_unsafe && !c.unsafe_context) + diags_.error(call.range, + "E0308", + "call to unsafe function/method '" + display_name + + "' requires unsafe context"); + if (actuals.size() != sig->params.size()) { + const std::size_t explicit_expected = + sig->params.size() - (call.callee->kind == ast::ExprKind::Member ? 1u : 0u); + diags_.error(call.range, + "E0309", + "function/method '" + display_name + "' expects " + + std::to_string(explicit_expected) + " arguments, got " + + std::to_string(call.args.size())); + return sig->result; + } + std::unordered_map subst; + if (!call.generic_args.empty()) { + if (call.generic_args.size() != sig->generic_names.size()) + diags_.error(call.range, "E0310", "wrong number of explicit generic arguments"); + else + for (std::size_t i = 0; i < call.generic_args.size(); ++i) + subst[sig->generic_names[i]] = type_from_ast(call.generic_args[i]); + } + for (std::size_t i = 0; i < actuals.size(); ++i) { + const Type actual = actuals[i].second; + const Type expected = substitute_type(sig->params[i], subst); + if (!unify_generic(sig->params[i], actual, subst) && + !can_coerce_expr(actuals[i].first, actual, expected)) + diags_.error(actuals[i].first->range, + "E0311", + "argument " + std::to_string(i + 1) + " has type '" + actual.str() + + "', expected '" + expected.str() + "'"); + } + TraitSolver solver(model_); + for (const auto& [g, bounds] : sig->bounds) { + auto si = subst.find(g); + if (si == subst.end()) { + diags_.error(call.range, "E0312", "cannot infer generic parameter '" + g + "'"); + continue; + } + for (const auto& bound : bounds) + if (!solver.satisfies(si->second, bound)) + diags_.error(call.range, + "E0313", + "type '" + si->second.str() + "' does not satisfy trait '" + bound + + "'"); + } + Type result = substitute_type(sig->result, subst); + return sig->is_async ? Type::named("Future", {result}) : result; +} + +Type TypeChecker::check_builtin_call(const ast::CallExpr& call, FnContext& c) { + const auto& name = static_cast(*call.callee).name; + const auto string_like = [](const Type& type) { + return type.kind == TypeKind::Str || + (type.kind == TypeKind::Ref && type.pointee && type.pointee->kind == TypeKind::Str); + }; + const auto printable = [&](const Type& type) { + return type.is_integer() || type.is_float() || type.kind == TypeKind::Bool || + type.kind == TypeKind::Char || string_like(type); + }; + std::vector arguments; + for (const auto& argument : call.args) + arguments.push_back(check_expr(*argument, c)); + + if (name == "print" || name == "println" || name == "eprint" || name == "eprintln") { + for (std::size_t i = 0; i < arguments.size(); ++i) + if (!printable(arguments[i])) + diags_.error(call.args[i]->range, + "E0367", + "print argument has unsupported type '" + arguments[i].str() + "'"); + return Type::builtin("unit"); + } + if (name == "input") { + if (arguments.size() > 1) + diags_.error(call.range, "E0368", "input expects zero or one prompt argument"); + if (!arguments.empty() && !string_like(arguments[0])) + diags_.error(call.args[0]->range, "E0369", "input prompt must be a string"); + return Type::ref(Type::builtin("str"), false); + } + if (name == "str") { + if (arguments.size() != 1) + diags_.error(call.range, "E0370", "str expects exactly one argument"); + else if (!printable(arguments[0])) + diags_.error(call.args[0]->range, + "E0371", + "str argument has unsupported type '" + arguments[0].str() + "'"); + return Type::ref(Type::builtin("str"), false); + } + if (name == "int" || name == "float") { + if (arguments.size() != 1) + diags_.error(call.range, "E0370", name + " expects exactly one argument"); + else if (!arguments[0].is_numeric() && !string_like(arguments[0])) + diags_.error( + call.args[0]->range, "E0372", name + " argument must be numeric or string-like"); + return Type::builtin(name == "int" ? "i64" : "f64"); + } + if (name == "bool") { + if (arguments.size() != 1) + diags_.error(call.range, "E0370", "bool expects exactly one argument"); + else if (arguments[0].kind != TypeKind::Bool && !arguments[0].is_numeric() && + !string_like(arguments[0])) + diags_.error( + call.args[0]->range, "E0373", "bool argument must be scalar or string-like"); + return Type::builtin("bool"); + } + if (name == "len") { + if (arguments.size() != 1) + diags_.error(call.range, "E0370", "len expects exactly one argument"); + else { + const Type& type = arguments[0]; + if (!string_like(type)) + diags_.error(call.args[0]->range, + "E0374", + "len is not implemented for type '" + type.str() + "'"); + } + return Type::builtin("usize"); + } + return {}; +} +void TypeChecker::validate_asm(const ast::AsmExpr& a, FnContext& c) { + if (!c.unsafe_context) + diags_.error(a.range, "E0314", "asm() is only permitted inside unsafe code"); + if (a.assembly.empty()) + diags_.warning(a.range, "W0315", "empty asm template"); + for (const auto& o : a.operands) { + if (o.constraint.empty()) + diags_.error(o.range, "E0316", "asm register/clobber constraint cannot be empty"); + for (char ch : o.constraint) + if (!(std::isalnum(static_cast(ch)) || + std::string("=+&{},~*").find(ch) != std::string::npos)) + diags_.error( + o.range, "E0317", "invalid character in asm constraint '" + o.constraint + "'"); + if (o.value) + check_expr(*o.value, c); + } +} +Type TypeChecker::check_expr(const ast::Expr& e, FnContext& c, bool) { + Type t; + switch (e.kind) { + case ast::ExprKind::Integer: + t = Type::builtin("i32"); + break; + case ast::ExprKind::Float: + t = Type::builtin("f64"); + break; + case ast::ExprKind::String: + t = Type::ref(Type::builtin("str"), false); + break; + case ast::ExprKind::Char: + t = Type::builtin("char"); + break; + case ast::ExprKind::Bool: + t = Type::builtin("bool"); + break; + case ast::ExprKind::Name: { + auto& n = static_cast(e); + auto fi = model_.functions.find(n.name); + t = fi != model_.functions.end() ? Type::named("fn") : lookup_local(n.name, c, e.range); + break; + } + case ast::ExprKind::StructLiteral: { + auto& st = static_cast(e); + auto si = model_.structs.find(st.name); + if (si == model_.structs.end()) { + diags_.error(e.range, "E0337", "unknown struct '" + st.name + "'"); + t = {}; + break; + } + std::vector type_args; + for (const auto& arg : st.generic_args) + type_args.push_back(type_from_ast(arg)); + if (type_args.size() != si->second.generic_names.size()) { + diags_.error(e.range, + "E0338", + "struct '" + st.name + "' expects " + + std::to_string(si->second.generic_names.size()) + + " type arguments, got " + std::to_string(type_args.size())); + t = {}; + break; + } + std::unordered_map subst; + for (std::size_t i = 0; i < type_args.size(); ++i) + subst[si->second.generic_names[i]] = type_args[i]; + TraitSolver solver(model_); + for (const auto& [generic, bounds] : si->second.bounds) { + const auto actual = subst.find(generic); + if (actual == subst.end()) + continue; + for (const auto& bound : bounds) + if (!solver.satisfies(actual->second, bound)) + diags_.error(e.range, + "E0362", + "type '" + actual->second.str() + + "' does not satisfy struct bound '" + generic + ": " + + bound + "'"); + } + std::unordered_set seen; + for (const auto& f : st.fields) { + if (!seen.insert(f.name).second) + diags_.error( + f.range, "E0339", "duplicate field '" + f.name + "' in struct literal"); + auto fi = si->second.fields.find(f.name); + Type actual = check_expr(*f.value, c); + if (fi == si->second.fields.end()) + diags_.error( + f.range, "E0340", "struct '" + st.name + "' has no field '" + f.name + "'"); + else { + Type expected = substitute_type(fi->second, subst); + if (!can_coerce_expr(f.value.get(), actual, expected)) + diags_.error(f.range, + "E0341", + "field '" + f.name + "' expects '" + expected.str() + + "', found '" + actual.str() + "'"); + } + } + for (const auto& [name, ft] : si->second.fields) { + (void)ft; + if (!seen.contains(name)) + diags_.error(e.range, + "E0342", + "missing field '" + name + "' in struct literal '" + st.name + + "'"); + } + t = Type::named(st.name, std::move(type_args)); + break; + } + case ast::ExprKind::Unary: { + auto& u = static_cast(e); + Type x = check_expr(*u.operand, c); + if (u.op == "!") { + if (x.kind != TypeKind::Bool && !x.is_integer()) + diags_.error(e.range, "E0318", "not/! requires bool or integer"); + t = x; + } else if (u.op == "~") { + if (!x.is_integer()) + diags_.error(e.range, "E0318", "~ requires an integer operand"); + t = x; + } else if (u.op == "-") { + if (!x.is_numeric()) + diags_.error(e.range, "E0319", "unary - requires numeric operand"); + t = x; + } else if (u.op == "move") { + t = x; + } else if (u.op == "*") { + if (x.kind != TypeKind::Ref && x.kind != TypeKind::RawPtr) { + diags_.error(e.range, "E0320", "dereference requires reference or raw pointer"); + t = {}; + } else { + if (x.kind == TypeKind::RawPtr && !c.unsafe_context) + diags_.error(e.range, + "E0321", + "dereferencing a raw pointer requires unsafe context"); + t = x.pointee ? *x.pointee : Type{}; + } + } + break; + } + case ast::ExprKind::Binary: { + auto& b = static_cast(e); + Type l = check_expr(*b.lhs, c), r = check_expr(*b.rhs, c); + if (l.is_integer() && r.is_integer() && l != r) { + if (integer_literal(b.lhs.get()) && can_coerce_expr(b.lhs.get(), l, r)) { + l = r; + model_.expr_types[b.lhs.get()] = r; + // `-literal` is a Unary node wrapping the Integer token; the + // MIR unary lowering reads the inner operand type, so keep it + // in sync or `sub i32` feeds an `i64` compare. + if (b.lhs->kind == ast::ExprKind::Unary) { + const auto& unary = static_cast(*b.lhs); + if (unary.operand) + model_.expr_types[unary.operand.get()] = r; + } + } else if (integer_literal(b.rhs.get()) && can_coerce_expr(b.rhs.get(), r, l)) { + r = l; + model_.expr_types[b.rhs.get()] = l; + if (b.rhs->kind == ast::ExprKind::Unary) { + const auto& unary = static_cast(*b.rhs); + if (unary.operand) + model_.expr_types[unary.operand.get()] = l; + } + } + } + if ((l.kind == TypeKind::RawPtr || r.kind == TypeKind::RawPtr) && + (b.op == "+" || b.op == "-") && !c.unsafe_context) + diags_.error(e.range, "E0322", "raw pointer arithmetic requires unsafe context"); + if (b.op == "==" || b.op == "!=" || b.op == "<" || b.op == "<=" || b.op == ">" || + b.op == ">=") { + if (!can_coerce(l, r) && !can_coerce(r, l)) + diags_.error(e.range, "E0323", "comparison operands have incompatible types"); + t = Type::builtin("bool"); + } else if (b.op == "&&" || b.op == "||") { + if (l.kind != TypeKind::Bool || r.kind != TypeKind::Bool) + diags_.error(e.range, "E0324", "logical operators require bool operands"); + t = Type::builtin("bool"); + } else if (b.op == "&" || b.op == "|" || b.op == "^" || b.op == "<<" || b.op == ">>") { + if (l != r || !l.is_integer()) + diags_.error(e.range, + "E0325", + "bitwise/shift operators require matching integer operands"); + t = l; + } else if (l.kind == TypeKind::RawPtr && (b.op == "+" || b.op == "-") && r.is_integer()) + t = l; + else if (l == r && l.is_numeric()) + t = l; + else { + diags_.error(e.range, + "E0325", + "binary operator '" + b.op + "' cannot combine '" + l.str() + + "' and '" + r.str() + "'"); + t = {}; + } + break; + } + case ast::ExprKind::Borrow: { + auto& b = static_cast(e); + Type target = check_expr(*b.target, c, false); + if (b.mut && !is_mutable_place(*b.target, c)) + diags_.error(e.range, "E0336", "mutable borrow requires a mutable place"); + t = Type::ref(target, b.mut); + break; + } + case ast::ExprKind::Call: + t = check_call(static_cast(e), c); + break; + case ast::ExprKind::Member: { + auto& m = static_cast(e); + Type base = check_expr(*m.base, c, false); + if (base.kind == TypeKind::Ref && base.pointee) + base = *base.pointee; + if (base.kind != TypeKind::Named || !model_.structs.contains(base.name)) { + diags_.error(e.range, "E0326", "member access requires struct type"); + t = {}; + } else { + auto& si = model_.structs.at(base.name); + auto f = si.fields.find(m.member); + if (f == si.fields.end()) { + diags_.error(e.range, + "E0327", + "struct '" + base.name + "' has no field '" + m.member + "'"); + t = {}; + } else { + std::unordered_map subst; + for (std::size_t gi = 0; gi < si.generic_names.size() && gi < base.args.size(); + ++gi) + subst[si.generic_names[gi]] = base.args[gi]; + t = substitute_type(f->second, subst); + } + } + break; + } + case ast::ExprKind::Index: { + auto& i = static_cast(e); + Type b = check_expr(*i.base, c, false), idx = check_expr(*i.index, c); + if (!idx.is_integer()) + diags_.error(i.index->range, "E0328", "index must be integer"); + Type container = b; + if (container.kind == TypeKind::Ref && container.pointee) + container = *container.pointee; + if (container.kind == TypeKind::Named && + (container.name == "Slice" || container.name == "SliceMut") && + !container.args.empty()) + t = container.args[0]; + else if (container.kind == TypeKind::Str) + t = Type::builtin("u8"); + else { + diags_.error(e.range, "E0329", "type '" + b.str() + "' is not safely indexable"); + t = {}; + } + break; + } + case ast::ExprKind::Await: { + if (!c.sig->is_async) + diags_.error(e.range, "E0343", "await is only valid inside an async function"); + Type f = check_expr(*static_cast(e).value, c); + if (f.kind != TypeKind::Named || f.name != "Future" || f.args.size() != 1) { + diags_.error(e.range, "E0330", "await requires Future"); + t = {}; + } else + t = f.args[0]; + break; + } + case ast::ExprKind::Cast: { + auto& x = static_cast(e); + Type from = check_expr(*x.value, c), to = type_from_ast(x.to); + bool ref_to_raw = from.kind == TypeKind::Ref && to.kind == TypeKind::RawPtr && + from.pointee && to.pointee; + if (ref_to_raw) { + if (!can_coerce(*from.pointee, *to.pointee)) + diags_.error( + e.range, "E0332", "reference/raw-pointer pointee types are incompatible"); + if (to.mut && !from.mut) + diags_.error(e.range, + "E0332", + "cannot create mutable raw pointer from shared reference"); + } else if ((from.kind == TypeKind::RawPtr || to.kind == TypeKind::RawPtr) && + !(from.kind == TypeKind::Str && to.kind == TypeKind::RawPtr) && + !c.unsafe_context) + diags_.error(e.range, "E0331", "this raw pointer cast requires unsafe context"); + const bool str_to_raw = from.kind == TypeKind::Str && to.kind == TypeKind::RawPtr && + to.pointee && to.pointee->kind == TypeKind::U8; + if (!(from.is_numeric() && to.is_numeric()) && !ref_to_raw && !str_to_raw && + from.kind != TypeKind::RawPtr && to.kind != TypeKind::RawPtr) + diags_.error(e.range, + "E0332", + "unsupported cast from '" + from.str() + "' to '" + to.str() + "'"); + t = to; + break; + } + case ast::ExprKind::Asm: { + auto& a = static_cast(e); + validate_asm(a, c); + t = Type::builtin("unit"); + break; + } + } + model_.expr_types[&e] = t; + return t; +} +} // namespace uinx diff --git a/src/sema_internal.hpp b/src/sema_internal.hpp new file mode 100644 index 0000000..21920c6 --- /dev/null +++ b/src/sema_internal.hpp @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +// Internal helpers shared by the translation units split out of sema.cpp. + +#pragma once + +#include "uinx/sema.hpp" + +#include +#include +#include + +namespace uinx::sema_detail { +inline Type replace_self(Type type, const Type& owner) { + if (type.kind == TypeKind::Named && type.name == "Self") + return owner; + if (type.pointee) + type.pointee = std::make_shared(replace_self(*type.pointee, owner)); + for (auto& arg : type.args) + arg = replace_self(std::move(arg), owner); + return type; +} + +inline bool integer_literal(const ast::Expr* expr) { + if (!expr) + return false; + if (expr->kind == ast::ExprKind::Integer) + return true; + if (expr->kind == ast::ExprKind::Unary) { + const auto& unary = static_cast(*expr); + return unary.op == "-" && unary.operand && unary.operand->kind == ast::ExprKind::Integer; + } + return false; +} + +inline bool can_coerce_expr(const ast::Expr* expr, const Type& from, const Type& to) { + if (can_coerce(from, to)) + return true; + if (expr && expr->kind == ast::ExprKind::String && from.kind == TypeKind::Ref && from.pointee && + from.pointee->kind == TypeKind::Str && to.kind == TypeKind::Str) + return true; + return integer_literal(expr) && from.is_integer() && to.is_integer(); +} + +inline bool static_initializer_expr(const ast::Expr& expr) { + if (expr.kind == ast::ExprKind::Integer || expr.kind == ast::ExprKind::Float || + expr.kind == ast::ExprKind::Bool || expr.kind == ast::ExprKind::Char) + return true; + if (expr.kind == ast::ExprKind::StructLiteral) { + const auto& structure = static_cast(expr); + return std::all_of( + structure.fields.begin(), structure.fields.end(), [](const ast::FieldInit& field) { + if (!field.value) + return false; + if (field.value->kind == ast::ExprKind::Integer) + return static_cast(*field.value).value == "0"; + if (field.value->kind == ast::ExprKind::Bool) + return !static_cast(*field.value).value; + return false; + }); + } + return false; +} + +inline bool builtin_function(std::string_view name) { + static const std::unordered_set names{ + "print", "println", "eprint", "eprintln", "input", "str", "int", "float", "bool", "len"}; + return names.contains(name); +} +} // namespace uinx::sema_detail diff --git a/src/sema_resolve.cpp b/src/sema_resolve.cpp new file mode 100644 index 0000000..271143f --- /dev/null +++ b/src/sema_resolve.cpp @@ -0,0 +1,384 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "sema_internal.hpp" +#include "uinx/sema.hpp" + +#include +#include +#include + +namespace uinx { +using namespace sema_detail; + +hir::SymbolId NameResolver::add(hir::Module& out, + hir::SymbolKind kind, + std::string name, + SourceRange range, + std::optional parent) { + hir::SymbolId id = static_cast(out.symbols.size()); + out.symbols.push_back({id, kind, std::move(name), std::move(range), parent}); + return id; +} +hir::Module NameResolver::resolve(const ast::Module& module) { + hir::Module out; + out.ast = &module; + for (const auto& item : module.items) { + std::visit( + [&](const auto& x) { + using T = std::decay_t; + if constexpr (std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v) { + hir::SymbolKind k = hir::SymbolKind::Function; + if constexpr (std::is_same_v) + k = hir::SymbolKind::Struct; + else if constexpr (std::is_same_v) + k = hir::SymbolKind::Trait; + else if constexpr (std::is_same_v) + k = hir::SymbolKind::Global; + if (out.globals.contains(x.name)) { + if constexpr (std::is_same_v) { + if (!x.is_extern) + diags_.error( + x.range, "E0200", "duplicate global symbol '" + x.name + "'"); + } else + diags_.error( + x.range, "E0200", "duplicate global symbol '" + x.name + "'"); + } else + out.globals[x.name] = add(out, k, x.name, x.range); + } + }, + item); + } + for (const auto& item : module.items) + if (auto f = std::get_if(&item)) + resolve_function(out, *f); + else if (auto im = std::get_if(&item)) + for (const auto& m : im->methods) + resolve_function(out, m); + return out; +} +void NameResolver::resolve_function(hir::Module& out, const ast::FunctionDecl& fn) { + std::optional owner; + auto gi = out.globals.find(fn.name); + if (gi != out.globals.end()) + owner = gi->second; + std::vector> scopes(1); + for (const auto& g : fn.generics) { + auto id = add(out, hir::SymbolKind::Generic, g.name, g.range, owner); + if (scopes.back().contains(g.name)) + diags_.error(g.range, "E0201", "duplicate generic parameter '" + g.name + "'"); + else + scopes.back()[g.name] = id; + } + for (const auto& p : fn.params) { + auto id = add(out, hir::SymbolKind::Parameter, p.name, p.range, owner); + if (scopes.back().contains(p.name)) + diags_.error(p.range, "E0202", "duplicate parameter '" + p.name + "'"); + else + scopes.back()[p.name] = id; + out.params[&p] = id; + } + if (fn.body) + resolve_block(out, *fn.body, scopes, owner); +} +void NameResolver::resolve_block( + hir::Module& out, + const ast::BlockStmt& block, + std::vector>& scopes, + std::optional owner) { + scopes.emplace_back(); + for (const auto& sp : block.stmts) { + const auto& s = *sp; + if (auto l = dynamic_cast(&s)) { + if (l->init) + resolve_expr(out, *l->init, scopes); + auto id = add(out, hir::SymbolKind::Local, l->name, l->range, owner); + if (scopes.back().contains(l->name)) + diags_.error( + l->range, "E0203", "binding '" + l->name + "' already exists in this scope"); + else + scopes.back()[l->name] = id; + out.bindings[l] = id; + } else if (auto a = dynamic_cast(&s)) { + if (a->op == "=" && a->target->kind == ast::ExprKind::Name) { + const auto& name = static_cast(*a->target).name; + bool resolved = out.globals.contains(name); + for (auto it = scopes.rbegin(); !resolved && it != scopes.rend(); ++it) + resolved = it->contains(name); + if (!resolved) { + resolve_expr(out, *a->value, scopes); + auto id = add(out, hir::SymbolKind::Local, name, a->target->range, owner); + scopes.back()[name] = id; + out.assignment_bindings[a] = id; + out.expr_resolution[a->target.get()] = id; + continue; + } + } + resolve_expr(out, *a->target, scopes); + resolve_expr(out, *a->value, scopes); + } else if (auto e = dynamic_cast(&s)) { + resolve_expr(out, *e->expr, scopes); + } else if (auto r = dynamic_cast(&s)) { + if (r->value) + resolve_expr(out, *r->value, scopes); + } else if (auto b = dynamic_cast(&s)) { + resolve_block(out, *b, scopes, owner); + } else if (auto i = dynamic_cast(&s)) { + resolve_expr(out, *i->condition, scopes); + resolve_block(out, *i->then_block, scopes, owner); + if (i->else_block) + resolve_block(out, *i->else_block, scopes, owner); + } else if (auto w = dynamic_cast(&s)) { + resolve_expr(out, *w->condition, scopes); + resolve_block(out, *w->body, scopes, owner); + } else if (auto f = dynamic_cast(&s)) { + resolve_expr(out, *f->begin, scopes); + resolve_expr(out, *f->end, scopes); + scopes.emplace_back(); + auto id = add(out, hir::SymbolKind::Local, f->name, f->range, owner); + scopes.back()[f->name] = id; + out.for_bindings[f] = id; + resolve_block(out, *f->body, scopes, owner); + scopes.pop_back(); + } else if (auto loop = dynamic_cast(&s)) { + resolve_block(out, *loop->body, scopes, owner); + } else if (auto u = dynamic_cast(&s)) { + resolve_block(out, *u->body, scopes, owner); + } + } + scopes.pop_back(); +} +void NameResolver::resolve_expr( + hir::Module& out, + const ast::Expr& e, + std::vector>& scopes) { + switch (e.kind) { + case ast::ExprKind::Name: { + auto& n = static_cast(e); + for (auto it = scopes.rbegin(); it != scopes.rend(); ++it) { + auto f = it->find(n.name); + if (f != it->end()) { + out.expr_resolution[&e] = f->second; + return; + } + } + auto g = out.globals.find(n.name); + if (g != out.globals.end()) { + out.expr_resolution[&e] = g->second; + return; + } + if (builtin_function(n.name)) + return; + diags_.error(e.range, "E0204", "unresolved name '" + n.name + "'"); + break; + } + case ast::ExprKind::StructLiteral: { + auto& st = static_cast(e); + auto g = out.globals.find(st.name); + if (g == out.globals.end()) + diags_.error(e.range, "E0206", "unknown struct '" + st.name + "'"); + else + out.expr_resolution[&e] = g->second; + for (auto& f : st.fields) + resolve_expr(out, *f.value, scopes); + break; + } + case ast::ExprKind::Unary: + resolve_expr(out, *static_cast(e).operand, scopes); + break; + case ast::ExprKind::Binary: { + auto& b = static_cast(e); + resolve_expr(out, *b.lhs, scopes); + resolve_expr(out, *b.rhs, scopes); + break; + } + case ast::ExprKind::Borrow: + resolve_expr(out, *static_cast(e).target, scopes); + break; + case ast::ExprKind::Call: { + auto& c = static_cast(e); + resolve_expr(out, *c.callee, scopes); + for (auto& a : c.args) + resolve_expr(out, *a, scopes); + break; + } + case ast::ExprKind::Member: + resolve_expr(out, *static_cast(e).base, scopes); + break; + case ast::ExprKind::Index: { + auto& i = static_cast(e); + resolve_expr(out, *i.base, scopes); + resolve_expr(out, *i.index, scopes); + break; + } + case ast::ExprKind::Await: + resolve_expr(out, *static_cast(e).value, scopes); + break; + case ast::ExprKind::Cast: + resolve_expr(out, *static_cast(e).value, scopes); + break; + case ast::ExprKind::Asm: { + auto& a = static_cast(e); + for (auto& o : a.operands) + if (o.value) + resolve_expr(out, *o.value, scopes); + for (auto& o : a.operands) + if (!o.out_name.empty()) { + bool found = false; + for (auto it = scopes.rbegin(); it != scopes.rend(); ++it) + if (auto f = it->find(o.out_name); f != it->end()) { + found = true; + break; + } + if (!found) + diags_.error( + o.range, "E0205", "unknown asm output binding '" + o.out_name + "'"); + } + break; + } + default: + break; + } +} + +bool TraitSolver::satisfies(const Type& type, std::string_view trait) const { + return satisfies(type, trait, {}); +} + +bool TraitSolver::satisfies( + const Type& type, + std::string_view trait, + const std::unordered_map>& generic_bounds) const { + if (trait == "Copy") + return type.is_copy(); + if (trait != "Send" && trait != "Sync") + return false; + + std::function&)> + unify_pattern; + unify_pattern = [&](const Type& pattern, + const Type& actual, + std::unordered_map& substitutions) -> bool { + if (pattern.kind == TypeKind::Generic) { + auto [it, inserted] = substitutions.emplace(pattern.name, actual); + return inserted || it->second == actual; + } + if (pattern.kind != actual.kind || pattern.name != actual.name || + pattern.mut != actual.mut || pattern.args.size() != actual.args.size()) + return false; + if (static_cast(pattern.pointee) != static_cast(actual.pointee)) + return false; + if (pattern.pointee && !unify_pattern(*pattern.pointee, *actual.pointee, substitutions)) + return false; + for (std::size_t i = 0; i < pattern.args.size(); ++i) + if (!unify_pattern(pattern.args[i], actual.args[i], substitutions)) + return false; + return true; + }; + + std::function solve; + solve = [&](const Type& current, std::string_view current_trait, std::size_t depth) -> bool { + if (depth > 64) + return false; + + for (const auto& impl : model_.impls) + if (impl.trait == current_trait && impl.for_type == current) + return true; + + for (const auto& marker : model_.markers) { + if (marker.trait != current_trait) + continue; + std::unordered_map substitutions; + if (!unify_pattern(marker.type, current, substitutions)) + continue; + bool valid = true; + for (const auto& [generic, bounds] : marker.bounds) { + auto actual = substitutions.find(generic); + if (actual == substitutions.end()) { + valid = false; + break; + } + for (const auto& bound : bounds) { + if (!solve(actual->second, bound, depth + 1)) { + valid = false; + break; + } + } + if (!valid) + break; + } + if (valid) + return true; + } + + if (current_trait != "Send" && current_trait != "Sync") + return false; + + std::unordered_set visiting; + std::function&, + std::unordered_set)> + auto_trait; + std::unordered_set assumed; + for (const auto& [generic, bounds] : generic_bounds) + for (const auto& bound : bounds) + assumed.insert(bound + ":" + generic); + auto_trait = [&](const Type& value, + std::string_view value_trait, + std::unordered_set& active, + std::unordered_set assumptions) -> bool { + const std::string key = std::string(value_trait) + ":" + value.str(); + if (!active.insert(key).second) + return true; + const auto finish = [&](bool result) { + active.erase(key); + return result; + }; + + if (value.kind == TypeKind::RawPtr) + return finish(false); + if (value.kind == TypeKind::Ref && value.pointee) { + if (value_trait == "Send" && value.mut) + return finish( + auto_trait(*value.pointee, "Send", active, std::move(assumptions))); + return finish(auto_trait(*value.pointee, "Sync", active, std::move(assumptions))); + } + if (value.kind == TypeKind::Generic) + return finish(assumptions.contains(std::string(value_trait) + ":" + value.name)); + if (value.kind == TypeKind::Str) + return finish(true); + if (value.kind != TypeKind::Named) + return finish(value.is_copy()); + + auto structure = model_.structs.find(value.name); + if (structure == model_.structs.end()) + return finish(false); + for (const auto& [generic, bounds] : structure->second.bounds) + if (std::find(bounds.begin(), bounds.end(), value_trait) != bounds.end()) + assumptions.insert(std::string(value_trait) + ":" + generic); + + std::unordered_map substitutions; + for (std::size_t i = 0; + i < structure->second.generic_names.size() && i < value.args.size(); + ++i) + substitutions[structure->second.generic_names[i]] = value.args[i]; + for (const auto& [field_name, field_type] : structure->second.fields) { + (void)field_name; + if (!auto_trait(substitute_type(field_type, substitutions), + value_trait, + active, + assumptions)) + return finish(false); + } + return finish(true); + }; + return auto_trait(type, trait, visiting, assumed); + }; + return solve(type, trait, 0); +} +} // namespace uinx diff --git a/src/sema_smp.cpp b/src/sema_smp.cpp new file mode 100644 index 0000000..bd1002c --- /dev/null +++ b/src/sema_smp.cpp @@ -0,0 +1,372 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "sema_internal.hpp" +#include "uinx/sema.hpp" + +#include +#include +#include + +namespace uinx { +using namespace sema_detail; + +void TypeChecker::validate_concurrency_boundaries() { + TraitSolver solver(model_); + for (const auto& [key, signature] : model_.functions) { + (void)key; + if (!signature.is_concurrent || !signature.decl) + continue; + // `extern` declarations are FFI trust boundaries without bodies: the + // `unsafe` block at the Uinx call site is the audit point, and raw + // pointers are the ABI mechanism itself (e.g. `uinx_atomic_*` + // intrinsics lowered directly to LLVM atomics). Enforcing `Send` on + // their parameters would make compare-exchange/fetch-add unusable + // from exactly the concurrent paths they exist for, while checking + // nothing about an actual transfer. Uinx-level callers stay checked. + if (signature.is_extern) + continue; + for (std::size_t i = 0; i < signature.params.size(); ++i) { + const Type& parameter = signature.params[i]; + if (solver.satisfies(parameter, "Send", signature.bounds)) + continue; + + const SourceRange range = i < signature.decl->params.size() + ? signature.decl->params[i].range + : signature.decl->range; + diags_.error(range, + "E0362", + "concurrent function parameter " + std::to_string(i + 1) + " of type '" + + parameter.str() + "' is not Send; use an owning Send type, a shared " + + "reference whose pointee is Sync, or add the required generic bound"); + } + } +} + +void TypeChecker::propagate_concurrency(ast::SmpMode mode) { + std::unordered_map> edges; + + std::function&)> visit_expr; + std::function&)> visit_stmt; + visit_expr = [&](const ast::Expr& expr, std::unordered_set& out) { + switch (expr.kind) { + case ast::ExprKind::Unary: + visit_expr(*static_cast(expr).operand, out); + break; + case ast::ExprKind::Binary: { + const auto& b = static_cast(expr); + visit_expr(*b.lhs, out); + visit_expr(*b.rhs, out); + break; + } + case ast::ExprKind::Borrow: + visit_expr(*static_cast(expr).target, out); + break; + case ast::ExprKind::Call: { + const auto& call = static_cast(expr); + if (call.callee->kind == ast::ExprKind::Name) { + const auto& name = static_cast(*call.callee).name; + if (model_.functions.contains(name)) + out.insert(name); + } else if (call.callee->kind == ast::ExprKind::Member) { + const auto& member = static_cast(*call.callee); + auto type_it = model_.expr_types.find(member.base.get()); + if (type_it != model_.expr_types.end()) { + Type owner = type_it->second; + if (owner.kind == TypeKind::Ref && owner.pointee) + owner = *owner.pointee; + const std::string key = owner.name + "::" + member.member; + if (model_.functions.contains(key)) + out.insert(key); + } + visit_expr(*member.base, out); + } + for (const auto& arg : call.args) + visit_expr(*arg, out); + break; + } + case ast::ExprKind::Member: + visit_expr(*static_cast(expr).base, out); + break; + case ast::ExprKind::Index: { + const auto& i = static_cast(expr); + visit_expr(*i.base, out); + visit_expr(*i.index, out); + break; + } + case ast::ExprKind::Await: + visit_expr(*static_cast(expr).value, out); + break; + case ast::ExprKind::Cast: + visit_expr(*static_cast(expr).value, out); + break; + case ast::ExprKind::StructLiteral: + for (const auto& field : static_cast(expr).fields) + visit_expr(*field.value, out); + break; + case ast::ExprKind::Asm: + for (const auto& operand : static_cast(expr).operands) + if (operand.value) + visit_expr(*operand.value, out); + break; + default: + break; + } + }; + visit_stmt = [&](const ast::Stmt& stmt, std::unordered_set& out) { + if (const auto* let_stmt = dynamic_cast(&stmt)) { + if (let_stmt->init) + visit_expr(*let_stmt->init, out); + } else if (const auto* assign_stmt = dynamic_cast(&stmt)) { + visit_expr(*assign_stmt->target, out); + visit_expr(*assign_stmt->value, out); + } else if (const auto* expr_stmt = dynamic_cast(&stmt)) + visit_expr(*expr_stmt->expr, out); + else if (const auto* return_stmt = dynamic_cast(&stmt)) { + if (return_stmt->value) + visit_expr(*return_stmt->value, out); + } else if (const auto* if_stmt = dynamic_cast(&stmt)) { + visit_expr(*if_stmt->condition, out); + for (const auto& child : if_stmt->then_block->stmts) + visit_stmt(*child, out); + if (if_stmt->else_block) + for (const auto& child : if_stmt->else_block->stmts) + visit_stmt(*child, out); + } else if (const auto* while_stmt = dynamic_cast(&stmt)) { + visit_expr(*while_stmt->condition, out); + for (const auto& child : while_stmt->body->stmts) + visit_stmt(*child, out); + } else if (const auto* for_stmt = dynamic_cast(&stmt)) { + visit_expr(*for_stmt->begin, out); + visit_expr(*for_stmt->end, out); + for (const auto& child : for_stmt->body->stmts) + visit_stmt(*child, out); + } else if (const auto* loop_stmt = dynamic_cast(&stmt)) { + for (const auto& child : loop_stmt->body->stmts) + visit_stmt(*child, out); + } else if (const auto* block_stmt = dynamic_cast(&stmt)) { + for (const auto& child : block_stmt->stmts) + visit_stmt(*child, out); + } else if (const auto* unsafe_stmt = dynamic_cast(&stmt)) { + for (const auto& child : unsafe_stmt->body->stmts) + visit_stmt(*child, out); + } + }; + + for (const auto& [key, sig] : model_.functions) { + if (!sig.decl || !sig.decl->body) + continue; + auto& callees = edges[key]; + for (const auto& stmt : sig.decl->body->stmts) + visit_stmt(*stmt, callees); + } + + std::vector work; + for (const auto& [key, sig] : model_.functions) + if (sig.is_concurrent) + work.push_back(key); + std::unordered_set seen; + while (!work.empty()) { + std::string key = std::move(work.back()); + work.pop_back(); + if (!seen.insert(key).second) + continue; + auto fit = model_.functions.find(key); + if (fit != model_.functions.end()) + fit->second.is_concurrent = true; + auto edge = edges.find(key); + if (edge == edges.end()) + continue; + for (const auto& callee : edge->second) + work.push_back(callee); + } + + // All SMP modes are explicit-shared now: `auto`, `manual` and `strict` + // differ only in ordering of explicitly atomic accesses, never in + // promotion. See docs/MEMORY_MODEL.md. + (void)mode; + // Industrial-grade explicit-shared model (Rust + C++20 + LKMM fusion): + // `smp auto/manual/strict` no longer implicitly promotes mutable globals. + // Only explicitly `shared` globals/fields lower to LLVM atomics. Any + // concurrent access to a mutable non-shared, non-percpu global is E0363. + // Ordering: auto/manual => acquire/release/acq_rel for explicit atomics, + // strict => seq_cst. See docs/MEMORY_MODEL.md. + // Note: `shared bool` is rejected at definition (use AtomicBool over u8) + // because LLVM atomics must be byte-sized (i1 is not valid). + const auto atomic_compatible = [](const Type& type) { + return type.is_integer() || type.kind == TypeKind::RawPtr; + }; + + std::unordered_set reported; + std::function check_expr; + std::function check_stmt; + check_expr = [&](const ast::Expr& expr) { + if (expr.kind == ast::ExprKind::Name) { + const auto& name = static_cast(expr).name; + auto global = model_.globals.find(name); + if (global != model_.globals.end() && global->second.is_mut && + !global->second.is_percpu && !global->second.is_shared) { + const std::string key = std::to_string(expr.range.begin.line) + ":" + + std::to_string(expr.range.begin.column) + ":" + name; + if (reported.insert(key).second) { + if (atomic_compatible(global->second.type)) { + diags_.error(expr.range, + "E0363", + "concurrent access to mutable global '" + name + + "' requires explicit `shared`, a lock, or `percpu` " + "storage; smp auto no longer promotes implicitly " + "(Rust+C++20+LKMM explicit-shared model)"); + } else { + diags_.error(expr.range, + "E0363", + "concurrent access to aggregate global '" + name + + "' cannot be strengthened as one atomic object; mark " + "atomic fields shared, use a lock, or use percpu " + "storage"); + } + } + } + return; + } + if (expr.kind == ast::ExprKind::Member) { + const auto& member = static_cast(expr); + if (member.base->kind == ast::ExprKind::Name) { + const auto& base_name = static_cast(*member.base).name; + auto global = model_.globals.find(base_name); + if (global != model_.globals.end() && global->second.is_mut && + !global->second.is_percpu && global->second.type.kind == TypeKind::Named) { + auto structure = model_.structs.find(global->second.type.name); + if (structure != model_.structs.end()) { + auto field = structure->second.fields.find(member.member); + if (field != structure->second.fields.end()) { + if (structure->second.shared_fields.contains(member.member)) + return; + const std::string key = std::to_string(expr.range.begin.line) + ":" + + std::to_string(expr.range.begin.column) + ":" + + base_name + "." + member.member; + if (reported.insert(key).second) { + diags_.error( + expr.range, + "E0363", + "concurrent access to field '" + base_name + "." + + member.member + + "' requires explicit `shared` field, a lock, or " + "`percpu` storage; smp auto no longer promotes implicitly"); + } + return; + } + } + // Aggregate global itself accessed concurrently without per-field + // sharing: reject. + const std::string key = std::to_string(expr.range.begin.line) + ":" + + std::to_string(expr.range.begin.column) + ":" + + base_name; + if (reported.insert(key).second) { + diags_.error(expr.range, + "E0363", + "concurrent access to aggregate global '" + base_name + + "' cannot be strengthened as one atomic object; mark " + "atomic fields shared, use a lock, or use percpu " + "storage"); + } + return; + } + } + check_expr(*member.base); + return; + } + switch (expr.kind) { + case ast::ExprKind::Unary: + check_expr(*static_cast(expr).operand); + break; + case ast::ExprKind::Binary: { + const auto& binary = static_cast(expr); + check_expr(*binary.lhs); + check_expr(*binary.rhs); + break; + } + case ast::ExprKind::Borrow: + check_expr(*static_cast(expr).target); + break; + case ast::ExprKind::Call: { + const auto& call = static_cast(expr); + if (call.callee->kind == ast::ExprKind::Member) + check_expr(*static_cast(*call.callee).base); + for (const auto& arg : call.args) + check_expr(*arg); + break; + } + case ast::ExprKind::Index: { + const auto& index = static_cast(expr); + check_expr(*index.base); + check_expr(*index.index); + break; + } + case ast::ExprKind::Await: + check_expr(*static_cast(expr).value); + break; + case ast::ExprKind::Cast: + check_expr(*static_cast(expr).value); + break; + case ast::ExprKind::StructLiteral: + for (const auto& field : static_cast(expr).fields) + check_expr(*field.value); + break; + case ast::ExprKind::Asm: + for (const auto& operand : static_cast(expr).operands) + if (operand.value) + check_expr(*operand.value); + break; + default: + break; + } + }; + check_stmt = [&](const ast::Stmt& stmt) { + if (const auto* let_stmt = dynamic_cast(&stmt)) { + if (let_stmt->init) + check_expr(*let_stmt->init); + } else if (const auto* assign_stmt = dynamic_cast(&stmt)) { + check_expr(*assign_stmt->target); + check_expr(*assign_stmt->value); + } else if (const auto* expr_stmt = dynamic_cast(&stmt)) + check_expr(*expr_stmt->expr); + else if (const auto* return_stmt = dynamic_cast(&stmt)) { + if (return_stmt->value) + check_expr(*return_stmt->value); + } else if (const auto* if_stmt = dynamic_cast(&stmt)) { + check_expr(*if_stmt->condition); + for (const auto& child : if_stmt->then_block->stmts) + check_stmt(*child); + if (if_stmt->else_block) + for (const auto& child : if_stmt->else_block->stmts) + check_stmt(*child); + } else if (const auto* while_stmt = dynamic_cast(&stmt)) { + check_expr(*while_stmt->condition); + for (const auto& child : while_stmt->body->stmts) + check_stmt(*child); + } else if (const auto* for_stmt = dynamic_cast(&stmt)) { + check_expr(*for_stmt->begin); + check_expr(*for_stmt->end); + for (const auto& child : for_stmt->body->stmts) + check_stmt(*child); + } else if (const auto* loop_stmt = dynamic_cast(&stmt)) { + for (const auto& child : loop_stmt->body->stmts) + check_stmt(*child); + } else if (const auto* block_stmt = dynamic_cast(&stmt)) { + for (const auto& child : block_stmt->stmts) + check_stmt(*child); + } else if (const auto* unsafe_stmt = dynamic_cast(&stmt)) { + for (const auto& child : unsafe_stmt->body->stmts) + check_stmt(*child); + } + }; + + for (const auto& [key, signature] : model_.functions) { + if (!signature.is_concurrent || !signature.decl || !signature.decl->body) + continue; + for (const auto& statement : signature.decl->body->stmts) + check_stmt(*statement); + } +} +} // namespace uinx diff --git a/src/sema_stmt.cpp b/src/sema_stmt.cpp new file mode 100644 index 0000000..ed6be29 --- /dev/null +++ b/src/sema_stmt.cpp @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "sema_internal.hpp" +#include "uinx/sema.hpp" + +#include +#include +#include + +namespace uinx { +using namespace sema_detail; + +void TypeChecker::check_function(const ast::FunctionDecl& fn, const FunctionSig& sig) { + if (!fn.body) + return; + FnContext c; + c.sig = &sig; + c.unsafe_context = fn.is_unsafe; + c.scopes.emplace_back(); + c.mutable_scopes.emplace_back(); + for (std::size_t i = 0; i < fn.params.size(); ++i) { + c.scopes.back()[fn.params[i].name] = sig.params[i]; + if (fn.params[i].mut) + c.mutable_scopes.back().insert(fn.params[i].name); + } + check_block(*fn.body, c); +} +void TypeChecker::check_block(const ast::BlockStmt& b, FnContext& c) { + c.scopes.emplace_back(); + c.mutable_scopes.emplace_back(); + for (const auto& s : b.stmts) + check_stmt(*s, c); + c.mutable_scopes.pop_back(); + c.scopes.pop_back(); +} +void TypeChecker::check_stmt(const ast::Stmt& s, FnContext& c) { + if (auto l = dynamic_cast(&s)) { + Type init = l->init ? check_expr(*l->init, c) : Type{}; + Type ty = l->type + ? type_from_ast(*l->type, + std::unordered_set(c.sig->generic_names.begin(), + c.sig->generic_names.end())) + : init; + if (l->type && !can_coerce_expr(l->init.get(), init, ty)) + diags_.error(l->range, + "E0301", + "initializer has type '" + init.str() + "' but binding expects '" + + ty.str() + "'"); + model_.binding_types[l] = ty; + c.scopes.back()[l->name] = ty; + if (l->mut) + c.mutable_scopes.back().insert(l->name); + return; + } + if (auto a = dynamic_cast(&s)) { + if (auto binding = model_.hir.assignment_bindings.find(a); + binding != model_.hir.assignment_bindings.end()) { + Type rhs = check_expr(*a->value, c); + model_.assignment_binding_types[a] = rhs; + model_.expr_types[a->target.get()] = rhs; + const auto& name = static_cast(*a->target).name; + c.scopes.back()[name] = rhs; + c.mutable_scopes.back().insert(name); + return; + } + Type lhs = check_expr(*a->target, c, false), rhs = check_expr(*a->value, c); + if (!is_mutable_place(*a->target, c)) + diags_.error(a->target->range, "E0333", "assignment requires a mutable place"); + if (!can_coerce_expr(a->value.get(), rhs, lhs)) + diags_.error( + a->range, "E0334", "cannot assign '" + rhs.str() + "' to '" + lhs.str() + "'"); + if (a->op != "=") { + const bool bitwise = a->op == "&=" || a->op == "|=" || a->op == "^=" || + a->op == "<<=" || a->op == ">>=" || a->op == "%="; + if (bitwise ? (!lhs.is_integer() || !rhs.is_integer()) + : (!lhs.is_numeric() || !rhs.is_numeric())) + diags_.error(a->range, + "E0335", + "compound assignment operator '" + a->op + + "' is not valid for these operands"); + } + return; + } + if (auto e = dynamic_cast(&s)) { + check_expr(*e->expr, c); + return; + } + if (auto r = dynamic_cast(&s)) { + Type got = r->value ? check_expr(*r->value, c) : Type::builtin("unit"); + if (!can_coerce_expr(r->value.get(), got, c.sig->result)) + diags_.error(r->range, + "E0302", + "return type '" + got.str() + "' does not match '" + c.sig->result.str() + + "'"); + return; + } + if (auto b = dynamic_cast(&s)) { + check_block(*b, c); + return; + } + if (auto i = dynamic_cast(&s)) { + Type t = check_expr(*i->condition, c); + if (t.kind != TypeKind::Bool) + diags_.error( + i->condition->range, "E0303", "if condition must be bool, found '" + t.str() + "'"); + check_block(*i->then_block, c); + if (i->else_block) + check_block(*i->else_block, c); + return; + } + if (auto w = dynamic_cast(&s)) { + Type t = check_expr(*w->condition, c); + if (t.kind != TypeKind::Bool) + diags_.error(w->condition->range, "E0304", "while condition must be bool"); + ++c.loop_depth; + check_block(*w->body, c); + --c.loop_depth; + return; + } + if (auto f = dynamic_cast(&s)) { + Type begin = check_expr(*f->begin, c), end = check_expr(*f->end, c); + if (!begin.is_integer() || !end.is_integer() || begin != end) + diags_.error(f->range, "E0356", "for ranges require matching integer endpoint types"); + c.scopes.emplace_back(); + c.mutable_scopes.emplace_back(); + c.scopes.back()[f->name] = begin; + ++c.loop_depth; + check_block(*f->body, c); + --c.loop_depth; + c.mutable_scopes.pop_back(); + c.scopes.pop_back(); + return; + } + if (auto l = dynamic_cast(&s)) { + ++c.loop_depth; + check_block(*l->body, c); + --c.loop_depth; + return; + } + if (dynamic_cast(&s) || dynamic_cast(&s)) { + if (c.loop_depth == 0) + diags_.error(s.range, "E0357", "break/continue is only valid inside a loop"); + return; + } + if (auto u = dynamic_cast(&s)) { + bool old = c.unsafe_context; + c.unsafe_context = true; + check_block(*u->body, c); + c.unsafe_context = old; + return; + } +} +Type TypeChecker::lookup_local(std::string_view n, const FnContext& c, const SourceRange& r) { + for (auto it = c.scopes.rbegin(); it != c.scopes.rend(); ++it) { + auto f = it->find(std::string(n)); + if (f != it->end()) + return f->second; + } + auto g = model_.globals.find(std::string(n)); + if (g != model_.globals.end()) + return g->second.type; + diags_.error(r, "E0305", "name '" + std::string(n) + "' is not a value in this scope"); + return {}; +} +bool TypeChecker::is_mutable_binding(std::string_view n, const FnContext& c) const { + for (auto it = c.mutable_scopes.rbegin(); it != c.mutable_scopes.rend(); ++it) + if (it->contains(std::string(n))) + return true; + auto g = model_.globals.find(std::string(n)); + return g != model_.globals.end() && g->second.is_mut && !g->second.is_const; +} +bool TypeChecker::is_mutable_place(const ast::Expr& e, const FnContext& c) { + if (e.kind == ast::ExprKind::Name) + return is_mutable_binding(static_cast(e).name, c); + if (e.kind == ast::ExprKind::Member) { + const auto& m = static_cast(e); + Type base{}; + if (auto it = model_.expr_types.find(m.base.get()); it != model_.expr_types.end()) + base = it->second; + if (base.kind == TypeKind::Ref) + return base.mut; + return is_mutable_place(*m.base, c); + } + if (e.kind == ast::ExprKind::Index) { + const auto& i = static_cast(e); + Type base{}; + if (auto it = model_.expr_types.find(i.base.get()); it != model_.expr_types.end()) + base = it->second; + if (base.kind == TypeKind::Ref) + return base.mut; + if (base.kind == TypeKind::Named && base.name == "SliceMut") + return is_mutable_place(*i.base, c); + return false; + } + if (e.kind == ast::ExprKind::Unary) { + const auto& u = static_cast(e); + if (u.op == "*") { + Type p{}; + if (auto it = model_.expr_types.find(u.operand.get()); it != model_.expr_types.end()) + p = it->second; + return p.kind == TypeKind::Ref ? p.mut : (p.kind == TypeKind::RawPtr ? p.mut : false); + } + } + return false; +} +bool TypeChecker::unify_generic(const Type& f, + const Type& a, + std::unordered_map& s) { + if (f.kind == TypeKind::Generic) { + auto it = s.find(f.name); + if (it == s.end()) { + s[f.name] = a; + return true; + } + return it->second == a; + } + if (f.kind != a.kind) + return false; + if (f.pointee && a.pointee) + return f.mut == a.mut && unify_generic(*f.pointee, *a.pointee, s); + if (f.args.size() != a.args.size()) + return false; + for (std::size_t i = 0; i < f.args.size(); ++i) + if (!unify_generic(f.args[i], a.args[i], s)) + return false; + return f.name == a.name || f.name.empty() || a.name.empty(); +} +} // namespace uinx From 930a727cf4839e18d2d95f42b2c35b0dd4cf1e40 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 12:07:50 +0000 Subject: [PATCH 3/4] borrowck: implicit and explicit reborrows, checked writes through references - Passing a mutref place to a mutref parameter reborrows it for the call instead of moving it; reusing it in the same argument list conflicts, and calls whose result may carry a reference keep the move semantics. - 'borrow [mut] deref r' reborrows through r: the new reference inherits r's origins without conflicting with r's own loans, and r and every ancestor stay frozen while the reborrow is live. Reference parameters are tracked through an external origin. - 'deref r = v' is now checked against loans on r and rejects a moved r. - mutable_ref_move now uses a real move ('val second = first'); consuming the same mutref twice is a reborrow and valid, as in Rust. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_018uUuRXQcBduAQvVUcJYkWz --- CHANGELOG.md | 2 + docs/MEMORY_MODEL.md | 9 + include/uinx/borrow.hpp | 25 ++- src/borrow.cpp | 2 + src/borrow_flow.cpp | 39 ++-- src/borrow_loans.cpp | 174 +++++++++++++++--- src/borrow_places.cpp | 18 +- tests/conformance/pass/reborrow_call.ux | 26 +++ tests/conformance/pass/reborrow_explicit.ux | 17 ++ tests/conformance/pass/reborrow_nested.ux | 17 ++ tests/conformance/pass/reborrow_param.ux | 16 ++ tests/safety/borrow-fail/mutable_ref_move.ux | 3 +- .../safety/borrow-fail/reborrow_after_move.ux | 19 ++ .../safety/borrow-fail/reborrow_double_mut.ux | 17 ++ .../borrow-fail/reborrow_param_write.ux | 16 ++ .../borrow-fail/reborrow_read_parent.ux | 16 ++ .../borrow-fail/reborrow_shared_then_write.ux | 15 ++ tests/safety/borrow-fail/reborrow_two_args.ux | 16 ++ .../borrow-fail/reborrow_write_parent.ux | 16 ++ .../borrow-fail/write_through_moved_ref.ux | 16 ++ 20 files changed, 429 insertions(+), 50 deletions(-) create mode 100644 tests/conformance/pass/reborrow_call.ux create mode 100644 tests/conformance/pass/reborrow_explicit.ux create mode 100644 tests/conformance/pass/reborrow_nested.ux create mode 100644 tests/conformance/pass/reborrow_param.ux create mode 100644 tests/safety/borrow-fail/reborrow_after_move.ux create mode 100644 tests/safety/borrow-fail/reborrow_double_mut.ux create mode 100644 tests/safety/borrow-fail/reborrow_param_write.ux create mode 100644 tests/safety/borrow-fail/reborrow_read_parent.ux create mode 100644 tests/safety/borrow-fail/reborrow_shared_then_write.ux create mode 100644 tests/safety/borrow-fail/reborrow_two_args.ux create mode 100644 tests/safety/borrow-fail/reborrow_write_parent.ux create mode 100644 tests/safety/borrow-fail/write_through_moved_ref.ux diff --git a/CHANGELOG.md b/CHANGELOG.md index 28a5f87..c990a59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ **Copyright © 2026 ViudiraTech · Code by JiTianYu391** +- Borrow checker: implicit reborrow when a `mutref` is passed to a `mutref` parameter, explicit `borrow [mut] deref r` reborrows (including nested and through reference parameters), and checking of writes through references (`deref r = v`) against live loans and moves. `consume(first); consume(first)` on a `mutref` is now accepted, as in Rust; `val second = first` still moves. +- Split `borrow.cpp`, `sema.cpp` and `codegen.cpp` into focused translation units (no behavior change). - Fixed a heap-use-after-free in `uinx::Type` copy assignment (`t = *t.pointee`) found by AddressSanitizer. - Repository hygiene: added `CMakePresets.json`, GitHub Actions CI (GCC, Clang, ASan/UBSan, clang-format), issue/PR templates, `CONTRIBUTING.md`, `.editorconfig` and `.gitattributes`; moved `ARCHITECTURE.md` and `BUILD.md` into `docs/`; moved test registration into `tests/CMakeLists.txt`; added `UINX_WARNINGS_AS_ERRORS` and `UINX_ENABLE_SANITIZERS` options; removed the stale `MANIFEST.sha256` and checked-in `verification/` transcripts. - Reworked borrow checking around backward liveness plus forward control-flow fixed points instead of block-local statement numbering. diff --git a/docs/MEMORY_MODEL.md b/docs/MEMORY_MODEL.md index 9e14c09..333aaa0 100644 --- a/docs/MEMORY_MODEL.md +++ b/docs/MEMORY_MODEL.md @@ -45,6 +45,15 @@ MIR/code generation: shared-like, inspects args, then activates to exclusive. This accepts `v.push(v.len())`. Function `borrow mut` args and `x += x` compound assignment use the same reservation/activation protocol; +- passing a `mutref` place to a `mutref` parameter reborrows it for the call + instead of moving it (`inc(r); inc(r)`); a second use of the same reference in + the same argument list conflicts, and a call whose result may carry a + reference still moves the argument; +- `borrow deref r` / `borrow mut deref r` reborrow through `r`: the new + reference inherits `r`'s origins without conflicting with `r`'s own loans, + and `r` (and every ancestor of a nested reborrow) is frozen while the + reborrow is live. Writes through a reference (`deref r = v`) respect loans + on `r` and reject a moved `r`; - reference provenance through bindings, aggregates, calls, method receivers; - stack references cannot escape scope/function (`E0404/E0405`); - branch/loop fixed-point merging; NLL-style loan expiry via backward diff --git a/include/uinx/borrow.hpp b/include/uinx/borrow.hpp index 6dd0a4e..040284b 100644 --- a/include/uinx/borrow.hpp +++ b/include/uinx/borrow.hpp @@ -33,6 +33,10 @@ class BorrowChecker { std::string place; SourceRange range{}; bool mut{false}; + // Reference places this origin was reborrowed through (`borrow deref r`), + // nearest first. Loans held by those references do not conflict with the + // reborrow, and they stay frozen for as long as the reborrow lives. + std::vector via; }; struct State { std::unordered_set moved; @@ -99,13 +103,28 @@ class BorrowChecker { bool is_copy_type(const Type& type) const; bool states_equivalent(const State& a, const State& b) const; - void - access_place(const std::string& place, const SourceRange& range, Access access, State& state); + void access_place(const std::string& place, + const SourceRange& range, + Access access, + State& state, + const std::vector& via = {}); void begin_borrow(const std::string& place, std::string borrower, bool mut, const SourceRange& range, - State& state); + State& state, + const std::vector& via = {}); + // `borrow deref r` reborrows through the reference place `r`. + std::optional reborrow_parent(const ast::Expr& target) const; + // Writing through a reference (`deref r = v`) must respect loans on `r`. + void write_through_reference(const ast::Expr& target, State& state); + // Implicit reborrow of a `mutref` argument passed to a `mutref` parameter. + bool is_reborrow_argument(const ast::CallExpr& call, std::size_t index) const; + void inspect_call_argument(const ast::CallExpr& call, + std::size_t index, + std::vector& guards, + State& state); + void release_call_guards(const std::vector& guards, State& state); // Two-phase borrow protocol (rustc two-phase borrows): // reservation acts as shared, activation upgrades to exclusive. void begin_two_phase_reservation(const std::string& place, diff --git a/src/borrow.cpp b/src/borrow.cpp index fe3e4c6..f6f5e91 100644 --- a/src/borrow.cpp +++ b/src/borrow.cpp @@ -107,6 +107,8 @@ void BorrowChecker::check_stmt(const ast::Stmt& stmt, State& state) { kill_borrowers_under(*target, state); inspect_expr(*assign->value, state, Access::Move); + if (!target) + write_through_reference(*assign->target, state); if (target) { access_place(*target, assign->target->range, Access::Write, state); mark_initialized(*target, state); diff --git a/src/borrow_flow.cpp b/src/borrow_flow.cpp index ab0ed46..8bab647 100644 --- a/src/borrow_flow.cpp +++ b/src/borrow_flow.cpp @@ -135,6 +135,17 @@ void BorrowChecker::inspect_expr(const ast::Expr& expr, State& state, Access acc const auto& borrow = static_cast(expr); if (auto place = place_of(*borrow.target)) { begin_borrow(*place, "", borrow.mut, expr.range, state); + } else if (const auto parent = reborrow_parent(*borrow.target)) { + access_place(*parent, expr.range, Access::Read, state); + for (const auto& origin : reference_origins(expr, state)) { + std::vector exempt = origin.via; + exempt.push_back(""); + begin_borrow( + origin.place, "", borrow.mut, expr.range, state, exempt); + for (const auto& ancestor : origin.via) + begin_borrow( + ancestor, "", borrow.mut, expr.range, state, exempt); + } } else { const auto origins = reference_origins(*borrow.target, state); if (!origins.empty()) { @@ -196,36 +207,26 @@ void BorrowChecker::inspect_expr(const ast::Expr& expr, State& state, Access acc } } } + std::vector guards; if (!reserved_places.empty()) { - for (const auto& arg : call.args) { + for (std::size_t i = 0; i < call.args.size(); ++i) { // Skip already-reserved `borrow mut` args (reservation // covers them); inspect others while reserved. - if (arg->kind == ast::ExprKind::Borrow && - static_cast(*arg).mut) + if (call.args[i]->kind == ast::ExprKind::Borrow && + static_cast(*call.args[i]).mut) continue; - const auto type = model_.expr_types.find(arg.get()); - Access arg_access = Access::Move; - if (type != model_.expr_types.end() && type->second.kind == TypeKind::Ref && - !type->second.mut) { - arg_access = Access::Read; - } - inspect_expr(*arg, state, arg_access); + inspect_call_argument(call, i, guards, state); } + release_call_guards(guards, state); for (const auto& rid : reservation_ids) activate_two_phase(rid, call.range, state); for (const auto& rid : reservation_ids) kill_borrower(rid, state); break; } - for (const auto& arg : call.args) { - const auto type = model_.expr_types.find(arg.get()); - Access arg_access = Access::Move; - if (type != model_.expr_types.end() && type->second.kind == TypeKind::Ref && - !type->second.mut) { - arg_access = Access::Read; - } - inspect_expr(*arg, state, arg_access); - } + for (std::size_t i = 0; i < call.args.size(); ++i) + inspect_call_argument(call, i, guards, state); + release_call_guards(guards, state); break; } case ast::ExprKind::Index: { diff --git a/src/borrow_loans.cpp b/src/borrow_loans.cpp index f91efee..5d64307 100644 --- a/src/borrow_loans.cpp +++ b/src/borrow_loans.cpp @@ -41,20 +41,30 @@ void BorrowChecker::promote_reference_value(const std::string& borrower, return; state.borrows.erase( - std::remove_if(state.borrows.begin(), - state.borrows.end(), - [&](const Borrow& loan) { - if (loan.borrower != "") - return false; - return std::any_of( - origins.begin(), origins.end(), [&](const RefOrigin& o) { - return overlaps(loan.place, o.place); - }); - }), + std::remove_if( + state.borrows.begin(), + state.borrows.end(), + [&](const Borrow& loan) { + if (loan.borrower != "") + return false; + return std::any_of(origins.begin(), origins.end(), [&](const RefOrigin& o) { + return overlaps(loan.place, o.place) || + std::find(o.via.begin(), o.via.end(), loan.place) != o.via.end(); + }); + }), state.borrows.end()); - for (const auto& origin : origins) - begin_borrow(origin.place, borrower, origin.mut, origin.range, state); + for (const auto& origin : origins) { + // Loans held by the references this one was reborrowed through, and by + // the borrower itself, are not conflicts. + std::vector exempt = origin.via; + exempt.push_back(borrower); + begin_borrow(origin.place, borrower, origin.mut, origin.range, state, exempt); + // A reborrow also freezes the reference it was taken through: `r` is + // unusable (or read-only) while the reborrow is live. + for (const auto& parent : origin.via) + begin_borrow(parent, borrower, origin.mut, origin.range, state, exempt); + } } void BorrowChecker::bind_reference_expression(const std::string& destination, @@ -105,7 +115,11 @@ void BorrowChecker::bind_reference_expression(const std::string& destination, void BorrowChecker::access_place(const std::string& place, const SourceRange& range, Access access, - State& state) { + State& state, + const std::vector& via) { + const auto held_by_parent = [&](const Borrow& loan) { + return std::find(via.begin(), via.end(), loan.borrower) != via.end(); + }; if (access == Access::Move) { const Type type = type_of_place(place, state); if (is_copy_type(type)) @@ -151,6 +165,8 @@ void BorrowChecker::access_place(const std::string& place, if (access == Access::BorrowShared) { for (const auto& loan : state.borrows) { + if (held_by_parent(loan)) + continue; if (places_conflict(place, loan.place) && is_active_mut(loan)) { report_error(range, "E0401", @@ -178,6 +194,8 @@ void BorrowChecker::access_place(const std::string& place, if (access == Access::BorrowMut) { for (const auto& loan : state.borrows) { + if (held_by_parent(loan)) + continue; if (places_conflict(place, loan.place)) { // A two-phase reservation of the same place by the same // statement is upgraded at activation, not a conflict here. @@ -210,8 +228,9 @@ void BorrowChecker::begin_borrow(const std::string& place, std::string borrower, bool mut, const SourceRange& range, - State& state) { - access_place(place, range, mut ? Access::BorrowMut : Access::BorrowShared, state); + State& state, + const std::vector& via) { + access_place(place, range, mut ? Access::BorrowMut : Access::BorrowShared, state, via); Borrow loan; loan.place = place; loan.borrower = std::move(borrower); @@ -305,15 +324,10 @@ void BorrowChecker::inspect_call_with_two_phase(const ast::CallExpr& call, State } // Args are inspected while the receiver reservation is shared-like, so // `v.push(v.len())` is accepted: `v.len()` shared-borrows while reserved. - for (const auto& arg : call.args) { - const auto type = model_.expr_types.find(arg.get()); - Access arg_access = Access::Move; - if (type != model_.expr_types.end() && type->second.kind == TypeKind::Ref && - !type->second.mut) { - arg_access = Access::Read; - } - inspect_expr(*arg, state, arg_access); - } + std::vector guards; + for (std::size_t i = 0; i < call.args.size(); ++i) + inspect_call_argument(call, i, guards, state); + release_call_guards(guards, state); if (reserved) { activate_two_phase(reservation_id, call.range, state); // Call-duration exclusive borrow ends with the call (unless the @@ -323,4 +337,116 @@ void BorrowChecker::inspect_call_with_two_phase(const ast::CallExpr& call, State // Receiver provenance: method may return borrow of receiver. // Keep existing reference_origins handling via caller. } + +std::optional BorrowChecker::reborrow_parent(const ast::Expr& target) const { + if (target.kind != ast::ExprKind::Unary) + return std::nullopt; + const auto& unary = static_cast(target); + if (unary.op != "*" || !unary.operand) + return std::nullopt; + const auto operand_type = model_.expr_types.find(unary.operand.get()); + if (operand_type == model_.expr_types.end() || operand_type->second.kind != TypeKind::Ref) + return std::nullopt; + return place_of(*unary.operand); +} + +void BorrowChecker::write_through_reference(const ast::Expr& target, State& state) { + const ast::Expr* base = ⌖ + while (base->kind == ast::ExprKind::Member || base->kind == ast::ExprKind::Index) { + base = base->kind == ast::ExprKind::Member + ? static_cast(*base).base.get() + : static_cast(*base).base.get(); + } + const auto parent = reborrow_parent(*base); + if (!parent) + return; + if (unavailable(*parent, state)) { + report_error( + target.range, "E0400", "use of moved value/place '" + display_place(*parent) + "'"); + return; + } + for (const auto& loan : state.borrows) { + // `` stands for the referent behind a reference, not for + // the reference itself. + if (loan.place.starts_with("second.kind != TypeKind::Ref || + !arg_type->second.mut) + return false; + // A result that may carry the reborrow keeps the old move semantics: its + // lifetime would have to be tied to the argument. + if (const auto call_type = model_.expr_types.find(&call); + call_type != model_.expr_types.end()) { + std::unordered_set visiting; + if (contains_reference(call_type->second, visiting)) + return false; + } + const FunctionSig* signature = nullptr; + std::size_t offset = 0; + if (call.callee->kind == ast::ExprKind::Name) { + const auto found = + model_.functions.find(static_cast(*call.callee).name); + if (found != model_.functions.end()) + signature = &found->second; + } else if (call.callee->kind == ast::ExprKind::Member) { + const auto& member = static_cast(*call.callee); + Type owner{}; + if (const auto type = model_.expr_types.find(member.base.get()); + type != model_.expr_types.end()) + owner = type->second; + if (owner.kind == TypeKind::Ref && owner.pointee) + owner = *owner.pointee; + const auto found = model_.functions.find(owner.name + "::" + member.member); + if (found != model_.functions.end()) { + signature = &found->second; + offset = 1; + } + } + if (!signature || index + offset >= signature->params.size()) + return false; + const Type& parameter = signature->params[index + offset]; + return parameter.kind == TypeKind::Ref && parameter.mut; +} + +void BorrowChecker::inspect_call_argument(const ast::CallExpr& call, + std::size_t index, + std::vector& guards, + State& state) { + const ast::Expr& arg = *call.args[index]; + if (is_reborrow_argument(call, index)) { + // `f(r)` with `r: mutref T` and `f(mutref T)` reborrows `*r` for the call + // instead of moving `r`. The guard makes a second use of `r` in the same + // argument list a conflict, matching rustc. + inspect_expr(arg, state, Access::Read); + const std::string id = ""; + begin_borrow(*place_of(arg), id, true, arg.range, state); + guards.push_back(id); + return; + } + const auto type = model_.expr_types.find(&arg); + const bool shared_ref = + type != model_.expr_types.end() && type->second.kind == TypeKind::Ref && !type->second.mut; + inspect_expr(arg, state, shared_ref ? Access::Read : Access::Move); +} + +void BorrowChecker::release_call_guards(const std::vector& guards, State& state) { + for (const auto& id : guards) + kill_borrower(id, state); +} } // namespace uinx diff --git a/src/borrow_places.cpp b/src/borrow_places.cpp index a763ee0..be5287d 100644 --- a/src/borrow_places.cpp +++ b/src/borrow_places.cpp @@ -148,12 +148,22 @@ std::vector BorrowChecker::reference_origins(const ast if (expr.kind == ast::ExprKind::Borrow) { const auto& borrow = static_cast(expr); if (auto place = place_of(*borrow.target)) { - result.push_back({*place, expr.range, borrow.mut}); + result.push_back({*place, expr.range, borrow.mut, {}}); return result; } result = reference_origins(*borrow.target, state); for (auto& origin : result) origin.mut = borrow.mut; + if (const auto parent = reborrow_parent(*borrow.target)) { + // Reference parameters have no recorded origin; track the reborrow + // against an external place so the parent is still frozen. + if (result.empty()) + result.push_back({"", expr.range, borrow.mut, {}}); + for (auto& origin : result) { + origin.range = expr.range; + origin.via.insert(origin.via.begin(), *parent); + } + } return result; } @@ -208,11 +218,13 @@ std::vector BorrowChecker::reference_origins(const ast // it back to its concrete argument when the call itself is bound. result.push_back({"", expr.range, - method->second.params.front().mut}); + method->second.params.front().mut, + {}}); } else { result.push_back({*receiver_place, expr.range, - method->second.params.front().mut}); + method->second.params.front().mut, + {}}); } } } diff --git a/tests/conformance/pass/reborrow_call.ux b/tests/conformance/pass/reborrow_call.ux new file mode 100644 index 0000000..ef5d332 --- /dev/null +++ b/tests/conformance/pass/reborrow_call.ux @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: conformance.positive.reborrow-call +# EXPECT: compile-pass +# Passing a mutref to a mutref parameter reborrows instead of moving it. + +func inc(r: mutref i32) -> unit: + deref r += 1 + return +func twice(r: mutref i32) -> unit: + inc(r) + inc(r) + return +func main() -> i32: + var x = 0 + twice(borrow mut x) + val r = borrow mut x + var i = 0 + loop: + if i >= 3: + break + inc(r) + i += 1 + return deref r - 5 diff --git a/tests/conformance/pass/reborrow_explicit.ux b/tests/conformance/pass/reborrow_explicit.ux new file mode 100644 index 0000000..1080eac --- /dev/null +++ b/tests/conformance/pass/reborrow_explicit.ux @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: conformance.positive.reborrow-explicit +# EXPECT: compile-pass +# borrow [mut] deref r reborrows; the parent is usable again after the last use. + +func main() -> i32: + var x = 1 + val r = borrow mut x + val r2 = borrow mut deref r + deref r2 = 5 + deref r = 6 + val s = borrow deref r + val t = borrow deref r + return deref s + deref t - 12 diff --git a/tests/conformance/pass/reborrow_nested.ux b/tests/conformance/pass/reborrow_nested.ux new file mode 100644 index 0000000..e06b681 --- /dev/null +++ b/tests/conformance/pass/reborrow_nested.ux @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: conformance.positive.reborrow-nested +# EXPECT: compile-pass +# Reborrows of reborrows keep every ancestor frozen while the innermost is live. + +func main() -> i32: + var x = 1 + val r = borrow mut x + val r2 = borrow mut deref r + val r3 = borrow mut deref r2 + deref r3 = 3 + deref r2 = 4 + deref r = 5 + return x - 5 diff --git a/tests/conformance/pass/reborrow_param.ux b/tests/conformance/pass/reborrow_param.ux new file mode 100644 index 0000000..6b55750 --- /dev/null +++ b/tests/conformance/pass/reborrow_param.ux @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: conformance.positive.reborrow-param +# EXPECT: compile-pass +# Reborrowing through a reference parameter. + +func f(p: mutref i32) -> i32: + val a = borrow mut deref p + deref a = 4 + deref p = 5 + return deref p +func main() -> i32: + var x = 0 + return f(borrow mut x) - 5 diff --git a/tests/safety/borrow-fail/mutable_ref_move.ux b/tests/safety/borrow-fail/mutable_ref_move.ux index f81ad54..41f533d 100644 --- a/tests/safety/borrow-fail/mutable_ref_move.ux +++ b/tests/safety/borrow-fail/mutable_ref_move.ux @@ -12,6 +12,7 @@ func consume(value: mutref i32) -> unit: func main() -> i32: var x = 1 val first = borrow mut x + val second = first consume(first) - consume(first) + consume(second) return 0 diff --git a/tests/safety/borrow-fail/reborrow_after_move.ux b/tests/safety/borrow-fail/reborrow_after_move.ux new file mode 100644 index 0000000..3c9c542 --- /dev/null +++ b/tests/safety/borrow-fail/reborrow_after_move.ux @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: safety.borrow.reborrow-after-move +# EXPECT: compile-fail +# EXPECT-DIAGNOSTIC: E0400 +# A moved mutref cannot be reborrowed by a later call. + +func inc(r: mutref i32) -> unit: + deref r += 1 + return +func main() -> i32: + var x = 1 + val r = borrow mut x + val moved = r + inc(r) + inc(moved) + return 0 diff --git a/tests/safety/borrow-fail/reborrow_double_mut.ux b/tests/safety/borrow-fail/reborrow_double_mut.ux new file mode 100644 index 0000000..34fab84 --- /dev/null +++ b/tests/safety/borrow-fail/reborrow_double_mut.ux @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: safety.borrow.reborrow-double-mut +# EXPECT: compile-fail +# EXPECT-DIAGNOSTIC: E0402 +# Two live mutable reborrows of the same reference conflict. + +func main() -> i32: + var x = 1 + val r = borrow mut x + val a = borrow mut deref r + val b = borrow mut deref r + deref a = 1 + deref b = 2 + return 0 diff --git a/tests/safety/borrow-fail/reborrow_param_write.ux b/tests/safety/borrow-fail/reborrow_param_write.ux new file mode 100644 index 0000000..220d78f --- /dev/null +++ b/tests/safety/borrow-fail/reborrow_param_write.ux @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: safety.borrow.reborrow-param-write +# EXPECT: compile-fail +# EXPECT-DIAGNOSTIC: E0403 +# Writing through a reference parameter while its reborrow is live. + +func f(p: mutref i32) -> unit: + val a = borrow mut deref p + deref p = 5 + deref a = 4 + return +func main() -> i32: + return 0 diff --git a/tests/safety/borrow-fail/reborrow_read_parent.ux b/tests/safety/borrow-fail/reborrow_read_parent.ux new file mode 100644 index 0000000..47516c7 --- /dev/null +++ b/tests/safety/borrow-fail/reborrow_read_parent.ux @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: safety.borrow.reborrow-read-parent +# EXPECT: compile-fail +# EXPECT-DIAGNOSTIC: E0406 +# Reading the parent while a mutable reborrow is live. + +func main() -> i32: + var x = 1 + val r = borrow mut x + val r2 = borrow mut deref r + val v = deref r + deref r2 = v + return 0 diff --git a/tests/safety/borrow-fail/reborrow_shared_then_write.ux b/tests/safety/borrow-fail/reborrow_shared_then_write.ux new file mode 100644 index 0000000..9162199 --- /dev/null +++ b/tests/safety/borrow-fail/reborrow_shared_then_write.ux @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: safety.borrow.reborrow-shared-then-write +# EXPECT: compile-fail +# EXPECT-DIAGNOSTIC: E0403 +# Writing through the parent while a shared reborrow is live. + +func main() -> i32: + var x = 1 + val r = borrow mut x + val s = borrow deref r + deref r = 2 + return deref s diff --git a/tests/safety/borrow-fail/reborrow_two_args.ux b/tests/safety/borrow-fail/reborrow_two_args.ux new file mode 100644 index 0000000..d799ba0 --- /dev/null +++ b/tests/safety/borrow-fail/reborrow_two_args.ux @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: safety.borrow.reborrow-two-args +# EXPECT: compile-fail +# EXPECT-DIAGNOSTIC: E0406 +# The same mutref cannot be reborrowed twice in one call. + +func two(a: mutref i32, b: mutref i32) -> unit: + return +func main() -> i32: + var x = 1 + val r = borrow mut x + two(r, r) + return 0 diff --git a/tests/safety/borrow-fail/reborrow_write_parent.ux b/tests/safety/borrow-fail/reborrow_write_parent.ux new file mode 100644 index 0000000..3e3d275 --- /dev/null +++ b/tests/safety/borrow-fail/reborrow_write_parent.ux @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: safety.borrow.reborrow-write-parent +# EXPECT: compile-fail +# EXPECT-DIAGNOSTIC: E0403 +# Writing through the parent while a mutable reborrow is live. + +func main() -> i32: + var x = 1 + val r = borrow mut x + val r2 = borrow mut deref r + deref r = 6 + deref r2 = 5 + return 0 diff --git a/tests/safety/borrow-fail/write_through_moved_ref.ux b/tests/safety/borrow-fail/write_through_moved_ref.ux new file mode 100644 index 0000000..b14e924 --- /dev/null +++ b/tests/safety/borrow-fail/write_through_moved_ref.ux @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: safety.borrow.write-through-moved-ref +# EXPECT: compile-fail +# EXPECT-DIAGNOSTIC: E0400 +# Writing through a moved mutref is a use after move. + +func main() -> i32: + var x = 1 + val r = borrow mut x + val moved = r + deref r = 2 + deref moved = 3 + return 0 From 54c197190e023391c05133672c666e0b1c81d5bf Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 12:11:43 +0000 Subject: [PATCH 4/4] smp: fix local-vs-global shadowing, share one AST walker; split mir.cpp - E0363 no longer fires for a local or parameter that shadows a mutable global (name resolution via HIR instead of string lookup). - The call-graph and shared-state checks used two hand-copied AST traversals; both now use for_each_child_expr/walk_stmt_exprs (sema_smp.cpp 372 -> 217 lines). - Split mir.cpp into lowering, statement, expression and optimizer units. - Tests for shadowing and for walker coverage (loop condition, cast). Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_018uUuRXQcBduAQvVUcJYkWz --- CHANGELOG.md | 3 +- CMakeLists.txt | 3 + docs/ARCHITECTURE.md | 8 +- src/mir.cpp | 1236 +---------------- src/mir_expr.cpp | 656 +++++++++ src/mir_internal.hpp | 29 + src/mir_optimizer.cpp | 298 ++++ src/mir_stmt.cpp | 321 +++++ src/sema_internal.hpp | 93 ++ src/sema_smp.cpp | 225 +-- .../pass/smp_local_shadows_global.ux | 19 + .../smp-fail/concurrent_global_in_cast.ux | 16 + .../concurrent_global_in_condition.ux | 17 + 13 files changed, 1496 insertions(+), 1428 deletions(-) create mode 100644 src/mir_expr.cpp create mode 100644 src/mir_internal.hpp create mode 100644 src/mir_optimizer.cpp create mode 100644 src/mir_stmt.cpp create mode 100644 tests/conformance/pass/smp_local_shadows_global.ux create mode 100644 tests/safety/smp-fail/concurrent_global_in_cast.ux create mode 100644 tests/safety/smp-fail/concurrent_global_in_condition.ux diff --git a/CHANGELOG.md b/CHANGELOG.md index c990a59..06a7f8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,8 @@ **Copyright © 2026 ViudiraTech · Code by JiTianYu391** - Borrow checker: implicit reborrow when a `mutref` is passed to a `mutref` parameter, explicit `borrow [mut] deref r` reborrows (including nested and through reference parameters), and checking of writes through references (`deref r = v`) against live loans and moves. `consume(first); consume(first)` on a `mutref` is now accepted, as in Rust; `val second = first` still moves. -- Split `borrow.cpp`, `sema.cpp` and `codegen.cpp` into focused translation units (no behavior change). +- Split `borrow.cpp`, `sema.cpp`, `codegen.cpp` and `mir.cpp` into focused translation units (no behavior change; LLVM IR for all examples and tests is byte-identical). +- SMP check: a local or parameter that shadows a mutable global is no longer reported as a global access (`E0363`), and the call-graph and shared-state walkers now share one AST traversal so they cannot drift apart. - Fixed a heap-use-after-free in `uinx::Type` copy assignment (`t = *t.pointee`) found by AddressSanitizer. - Repository hygiene: added `CMakePresets.json`, GitHub Actions CI (GCC, Clang, ASan/UBSan, clang-format), issue/PR templates, `CONTRIBUTING.md`, `.editorconfig` and `.gitattributes`; moved `ARCHITECTURE.md` and `BUILD.md` into `docs/`; moved test registration into `tests/CMakeLists.txt`; added `UINX_WARNINGS_AS_ERRORS` and `UINX_ENABLE_SANITIZERS` options; removed the stale `MANIFEST.sha256` and checked-in `verification/` transcripts. - Reworked borrow checking around backward liveness plus forward control-flow fixed points instead of block-local statement numbering. diff --git a/CMakeLists.txt b/CMakeLists.txt index 60a61c2..7f65101 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,9 @@ add_library(uinx_compiler src/borrow_liveness.cpp src/borrow_flow.cpp src/mir.cpp + src/mir_stmt.cpp + src/mir_expr.cpp + src/mir_optimizer.cpp src/codegen.cpp src/codegen_asm.cpp src/codegen_async.cpp diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 10dd0f5..c1a18b8 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -31,25 +31,25 @@ The parser recognizes functions, structs, generic parameters and bounds, traits, ## Resolution and semantic model -`src/sema.cpp` collects declarations, resolves names and builds `SemanticModel`. Function signatures retain generic parameters, trait bounds, receiver types, safety and async properties. Trait implementations are checked for duplicate implementations, duplicate methods, missing methods and signature mismatch. +`src/sema.cpp` collects declarations and builds `SemanticModel`; name resolution and the trait solver live in `src/sema_resolve.cpp`, statement and expression checking in `src/sema_stmt.cpp` and `src/sema_expr.cpp`, and concurrency propagation plus the `E0363` shared-state check in `src/sema_smp.cpp`. Function signatures retain generic parameters, trait bounds, receiver types, safety and async properties. Trait implementations are checked for duplicate implementations, duplicate methods, missing methods and signature mismatch. Type inference is local and constraint-oriented: declared parameter/result types are fixed while local bindings infer from initializers. Generic function calls infer type arguments from actual argument types when explicit arguments are not supplied. ## Ownership and borrowing -`src/borrow.cpp` tracks places, moves and loans. A place may be a local, a field path or a conservatively normalized indexed path. Shared borrows may coexist; active mutable loans are exclusive. Mutable references are affine and cannot be implicitly copied. +`src/borrow.cpp` drives statement-level checking and tracks places, moves and loans (place helpers in `borrow_places.cpp`, loan creation/access/reborrows in `borrow_loans.cpp`, liveness in `borrow_liveness.cpp`, branch/loop state merging and expression inspection in `borrow_flow.cpp`). A place may be a local, a field path or a conservatively normalized indexed path. Shared borrows may coexist; active mutable loans are exclusive. Mutable references are affine and cannot be implicitly copied. Last-use information shortens loans before lexical scope end, providing NLL-style behavior. Field paths permit disjoint field borrowing. Indexed paths intentionally alias conservatively unless a stronger proof is available. ## MIR -`src/mir.cpp` lowers typed AST into control-flow blocks and typed instructions. MIR contains storage slots, loads/stores, arithmetic, comparisons, branch/jump, calls, casts, field and index addresses, struct construction, inline assembly, await operations and explicit scope drops. +`src/mir.cpp`, `mir_stmt.cpp` and `mir_expr.cpp` lower typed AST into control-flow blocks and typed instructions. MIR contains storage slots, loads/stores, arithmetic, comparisons, branch/jump, calls, casts, field and index addresses, struct construction, inline assembly, await operations and explicit scope drops. The MIR optimizer removes instructions unreachable after terminators and canonicalizes the emitted block stream. LLVM optimization levels are then delegated to Clang during object generation. ## LLVM backend -`src/codegen.cpp` serializes valid textual LLVM IR. It emits named struct layouts, specialized generic layouts, function definitions/declarations, control flow, GEPs, slice bounds checks, inline assembly and async state machines. +`src/codegen.cpp` serializes valid textual LLVM IR (functions in `codegen_function.cpp`, async state machines in `codegen_async.cpp`, inline assembly and syscalls in `codegen_asm.cpp`). It emits named struct layouts, specialized generic layouts, function definitions/declarations, control flow, GEPs, slice bounds checks, inline assembly and async state machines. The backend does not generate C or C++. The driver writes `.ll`, asks Clang to compile LLVM IR into an object, and performs linking as a separate process. This preserves the required `IR -> Object -> Link` boundary. diff --git a/src/mir.cpp b/src/mir.cpp index 233f0ee..c2faaf2 100644 --- a/src/mir.cpp +++ b/src/mir.cpp @@ -4,6 +4,8 @@ #include "uinx/mir.hpp" +#include "mir_internal.hpp" + #include #include #include @@ -12,18 +14,8 @@ #include namespace uinx { -namespace { -bool unsigned_integer_kind(TypeKind kind) { - return kind == TypeKind::U8 || kind == TypeKind::U16 || kind == TypeKind::U32 || - kind == TypeKind::U64 || kind == TypeKind::U128 || kind == TypeKind::Usize; -} +using namespace mir_detail; -bool builtin_function(std::string_view name) { - static const std::unordered_set names{ - "print", "println", "eprint", "eprintln", "input", "str", "int", "float", "bool", "len"}; - return names.contains(name); -} -} // namespace std::string MIRLowerer::temp(FnState& s) { return "%t" + std::to_string(s.temp_counter++); } @@ -245,1226 +237,4 @@ void MIRLowerer::lower_block(const ast::BlockStmt& b, FnState& s) { emit_scope_drops(s); s.scope_locals.pop_back(); } -bool MIRLowerer::place_is_atomic(const ast::Expr& e, const FnState& s) const { - const Type ty = expr_type(e, s); - // Bool excluded: LLVM i1 atomics are invalid (byte-sized required). - // Use AtomicBool (u8-backed) instead. - const bool compatible = ty.is_integer() || ty.kind == TypeKind::RawPtr; - if (!compatible) - return false; - (void)s; - // Explicit-shared only: smp auto no longer promotes. Only `shared` - // globals/fields lower to LLVM atomics. percpu is never atomic. - if (e.kind == ast::ExprKind::Name) { - const auto& n = static_cast(e); - auto g = model_.globals.find(n.name); - if (g == model_.globals.end() || g->second.is_percpu) - return false; - return g->second.is_shared; - } - if (e.kind == ast::ExprKind::Member) { - const auto& m = static_cast(e); - Type base = expr_type(*m.base, s); - if (base.kind == TypeKind::Ref && base.pointee) - base = *base.pointee; - auto it = model_.structs.find(base.name); - return it != model_.structs.end() && it->second.shared_fields.contains(m.member); - } - return false; -} -std::string MIRLowerer::load_order(const ast::Expr& e, const FnState& s) const { - (void)e; - return s.smp_mode == ast::SmpMode::Strict ? "seq_cst" : "acquire"; -} -std::string MIRLowerer::store_order(const ast::Expr& e, const FnState& s) const { - (void)e; - return s.smp_mode == ast::SmpMode::Strict ? "seq_cst" : "release"; -} -std::string MIRLowerer::rmw_order(const ast::Expr& e, const FnState& s) const { - (void)e; - return s.smp_mode == ast::SmpMode::Strict ? "seq_cst" : "acq_rel"; -} - -void MIRLowerer::lower_stmt(const ast::Stmt& st, FnState& s) { - if (auto l = dynamic_cast(&st)) { - auto [v, init_ty] = lower_expr(*l->init, s); - Type t = init_ty; - if (auto it = model_.binding_types.find(l); it != model_.binding_types.end()) - t = subst_type(it->second, s); - v = coerce_value(s, v, init_ty, t, l->range); - std::string sl = slot(s, l->name); - s.slots[l->name] = sl; - s.local_types[l->name] = t; - s.scope_locals.back().push_back(l->name); - emit(s, {mir::Op::Alloca, sl, t, {}, "", {}, false, l->range}); - emit(s, {mir::Op::Store, "", t, {v, sl}, "", {}, false, l->range}); - return; - } - if (auto a = dynamic_cast(&st)) { - if (auto binding = model_.hir.assignment_bindings.find(a); - binding != model_.hir.assignment_bindings.end()) { - auto [v, init_ty] = lower_expr(*a->value, s); - Type t = init_ty; - if (auto it = model_.assignment_binding_types.find(a); - it != model_.assignment_binding_types.end()) - t = subst_type(it->second, s); - v = coerce_value(s, v, init_ty, t, a->range); - const auto& name = static_cast(*a->target).name; - std::string sl = slot(s, name); - s.slots[name] = sl; - s.local_types[name] = t; - s.scope_locals.back().push_back(name); - emit(s, {mir::Op::Alloca, sl, t, {}, "", {}, false, a->range}); - emit(s, {mir::Op::Store, "", t, {v, sl}, "", {}, false, a->range}); - return; - } - std::string addr = lower_place_address(*a->target, s); - Type target_ty = expr_type(*a->target, s); - auto [v, vt] = lower_expr(*a->value, s); - v = coerce_value(s, v, vt, target_ty, a->range); - const bool atomic = place_is_atomic(*a->target, s); - if (atomic && a->op != "=") { - const std::string op = a->op.substr(0, a->op.size() - 1); - if (op == "+" || op == "-" || op == "&" || op == "|" || op == "^") { - emit(s, - {mir::Op::AtomicRmw, - "", - target_ty, - {addr, v, rmw_order(*a->target, s)}, - op, - {}, - true, - a->range}); - return; - } - diags_.error(a->range, - "E0509", - "atomic compound assignment '" + a->op + - "' is not supported; use an explicit compare/exchange loop"); - return; - } - if (a->op != "=") { - std::string old = temp(s); - emit(s, {mir::Op::Load, old, target_ty, {addr}, "", {}, false, a->range}); - std::string combined = temp(s); - std::string op = a->op.substr(0, a->op.size() - 1); - emit(s, {mir::Op::Binary, combined, target_ty, {old, v}, op, {}, false, a->range}); - v = combined; - } - emit(s, - {mir::Op::Store, - "", - target_ty, - {v, addr}, - atomic ? store_order(*a->target, s) : "", - {}, - atomic, - a->range}); - return; - } - if (auto e = dynamic_cast(&st)) { - (void)lower_expr(*e->expr, s); - return; - } - if (auto r = dynamic_cast(&st)) { - emit_scope_drops(s); - if (r->value) { - auto [v, t] = lower_expr(*r->value, s); - v = coerce_value(s, v, t, s.fn.result, r->range); - emit(s, {mir::Op::Return, "", s.fn.result, {v}, "", {}, false, r->range}); - } else - emit(s, {mir::Op::Return, "", Type::builtin("unit"), {}, "", {}, false, r->range}); - return; - } - if (auto f = dynamic_cast(&st)) { - emit(s, - {f->compiler_only ? mir::Op::CompilerFence : mir::Op::Fence, - "", - Type::builtin("unit"), - {}, - f->order, - {}, - false, - f->range}); - return; - } - if (auto b = dynamic_cast(&st)) { - lower_block(*b, s); - return; - } - if (auto u = dynamic_cast(&st)) { - lower_block(*u->body, s); - return; - } - if (auto i = dynamic_cast(&st)) { - auto [c, ct] = lower_expr(*i->condition, s); - (void)ct; - std::size_t then_i = new_block(s, "if.then." + std::to_string(s.fn.blocks.size())); - std::size_t else_i = new_block(s, "if.else." + std::to_string(s.fn.blocks.size())); - std::size_t merge_i = new_block(s, "if.end." + std::to_string(s.fn.blocks.size())); - std::string then_l = s.fn.blocks[then_i].label, else_l = s.fn.blocks[else_i].label, - merge_l = s.fn.blocks[merge_i].label; - emit(s, - {mir::Op::CondJump, - "", - Type::builtin("bool"), - {c, then_l, else_l}, - "", - {}, - false, - i->range}); - s.block = then_i; - lower_block(*i->then_block, s); - if (!block(s).terminated) - emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {merge_l}, "", {}, false, i->range}); - s.block = else_i; - if (i->else_block) - lower_block(*i->else_block, s); - if (!block(s).terminated) - emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {merge_l}, "", {}, false, i->range}); - s.block = merge_i; - return; - } - if (auto w = dynamic_cast(&st)) { - std::size_t cond_i = new_block(s, "while.cond." + std::to_string(s.fn.blocks.size())); - std::size_t body_i = new_block(s, "while.body." + std::to_string(s.fn.blocks.size())); - std::size_t end_i = new_block(s, "while.end." + std::to_string(s.fn.blocks.size())); - std::string cl = s.fn.blocks[cond_i].label, bl = s.fn.blocks[body_i].label, - el = s.fn.blocks[end_i].label; - emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {cl}, "", {}, false, w->range}); - s.block = cond_i; - auto [c, t] = lower_expr(*w->condition, s); - (void)t; - emit(s, - {mir::Op::CondJump, "", Type::builtin("bool"), {c, bl, el}, "", {}, false, w->range}); - s.block = body_i; - s.loop_targets.push_back({cond_i, end_i}); - lower_block(*w->body, s); - s.loop_targets.pop_back(); - if (!block(s).terminated) - emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {cl}, "", {}, false, w->range}); - s.block = end_i; - return; - } - if (auto f = dynamic_cast(&st)) { - auto [begin_value, iter_ty] = lower_expr(*f->begin, s); - auto [end_value, end_ty] = lower_expr(*f->end, s); - (void)end_ty; - std::string iter_slot = slot(s, f->name); - s.slots[f->name] = iter_slot; - s.local_types[f->name] = iter_ty; - s.scope_locals.back().push_back(f->name); - emit(s, {mir::Op::Alloca, iter_slot, iter_ty, {}, "", {}, false, f->range}); - emit(s, {mir::Op::Store, "", iter_ty, {begin_value, iter_slot}, "", {}, false, f->range}); - std::size_t cond_i = new_block(s, "for.cond." + std::to_string(s.fn.blocks.size())); - std::size_t body_i = new_block(s, "for.body." + std::to_string(s.fn.blocks.size())); - std::size_t step_i = new_block(s, "for.step." + std::to_string(s.fn.blocks.size())); - std::size_t end_i = new_block(s, "for.end." + std::to_string(s.fn.blocks.size())); - const std::string cl = s.fn.blocks[cond_i].label, bl = s.fn.blocks[body_i].label, - sl = s.fn.blocks[step_i].label, el = s.fn.blocks[end_i].label; - emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {cl}, "", {}, false, f->range}); - s.block = cond_i; - std::string current = temp(s); - emit(s, {mir::Op::Load, current, iter_ty, {iter_slot}, "", {}, false, f->range}); - std::string cond = temp(s); - emit(s, - {mir::Op::Compare, - cond, - iter_ty, - {current, end_value}, - f->inclusive ? "<=" : "<", - {}, - false, - f->range}); - emit(s, - {mir::Op::CondJump, - "", - Type::builtin("bool"), - {cond, bl, el}, - "", - {}, - false, - f->range}); - s.block = body_i; - s.loop_targets.push_back({step_i, end_i}); - lower_block(*f->body, s); - s.loop_targets.pop_back(); - if (!block(s).terminated) - emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {sl}, "", {}, false, f->range}); - s.block = step_i; - std::string old = temp(s); - emit(s, {mir::Op::Load, old, iter_ty, {iter_slot}, "", {}, false, f->range}); - std::string one = temp(s); - emit(s, {mir::Op::ConstInt, one, iter_ty, {}, "1", {}, false, f->range}); - std::string next = temp(s); - emit(s, {mir::Op::Binary, next, iter_ty, {old, one}, "+", {}, false, f->range}); - emit(s, {mir::Op::Store, "", iter_ty, {next, iter_slot}, "", {}, false, f->range}); - emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {cl}, "", {}, false, f->range}); - s.block = end_i; - return; - } - if (auto l = dynamic_cast(&st)) { - std::size_t body_i = new_block(s, "loop.body." + std::to_string(s.fn.blocks.size())); - std::size_t end_i = new_block(s, "loop.end." + std::to_string(s.fn.blocks.size())); - const std::string bl = s.fn.blocks[body_i].label, el = s.fn.blocks[end_i].label; - emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {bl}, "", {}, false, l->range}); - s.block = body_i; - s.loop_targets.push_back({body_i, end_i}); - lower_block(*l->body, s); - s.loop_targets.pop_back(); - if (!block(s).terminated) - emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {bl}, "", {}, false, l->range}); - s.block = end_i; - return; - } - if (dynamic_cast(&st)) { - if (!s.loop_targets.empty()) { - emit_scope_drops(s); - emit(s, - {mir::Op::Jump, - "", - Type::builtin("unit"), - {s.fn.blocks[s.loop_targets.back().second].label}, - "", - {}, - false, - st.range}); - } - return; - } - if (dynamic_cast(&st)) { - if (!s.loop_targets.empty()) { - emit_scope_drops(s); - emit(s, - {mir::Op::Jump, - "", - Type::builtin("unit"), - {s.fn.blocks[s.loop_targets.back().first].label}, - "", - {}, - false, - st.range}); - } - return; - } -} -std::string MIRLowerer::lower_place_address(const ast::Expr& e, FnState& s) { - if (e.kind == ast::ExprKind::Unary) { - const auto& u = static_cast(e); - if (u.op == "*") - return lower_expr(*u.operand, s).first; - } - if (e.kind == ast::ExprKind::Name) { - auto& n = static_cast(e); - auto it = s.slots.find(n.name); - if (it != s.slots.end()) - return it->second; - if (model_.globals.contains(n.name)) - return "@" + n.name; - diags_.error(e.range, "E0502", "internal: no storage for '" + n.name + "'"); - return "null"; - } - if (e.kind == ast::ExprKind::Member) { - const auto& m = static_cast(e); - Type base_ty = expr_type(*m.base, s); - std::string base_addr; - if (base_ty.kind == TypeKind::Ref && base_ty.pointee) { - auto lowered = lower_expr(*m.base, s); - base_addr = lowered.first; - base_ty = *base_ty.pointee; - } else - base_addr = lower_place_address(*m.base, s); - if (base_ty.kind != TypeKind::Named) { - diags_.error(e.range, "E0503", "field address base is not a struct"); - return "null"; - } - auto si = model_.structs.find(base_ty.name); - if (si == model_.structs.end() || !si->second.decl) { - diags_.error(e.range, "E0503", "unknown struct layout for '" + base_ty.name + "'"); - return "null"; - } - std::size_t index = 0; - bool found = false; - for (const auto& field : si->second.decl->fields) { - if (field.name == m.member) { - found = true; - break; - } - ++index; - } - if (!found) { - diags_.error(e.range, "E0503", "unknown field '" + m.member + "'"); - return "null"; - } - Type field_ty = expr_type(e, s); - std::string result = temp(s); - mir::Instruction in; - in.op = mir::Op::FieldAddr; - in.result = result; - in.type = field_ty; - in.args = {base_addr}; - in.text = std::to_string(index); - in.range = e.range; - in.auxiliary_type = base_ty; - emit(s, std::move(in)); - return result; - } - if (e.kind == ast::ExprKind::Index) { - const auto& index_expr = static_cast(e); - Type container_ty = expr_type(*index_expr.base, s); - std::string container_addr; - if (container_ty.kind == TypeKind::Ref && container_ty.pointee) { - container_addr = lower_expr(*index_expr.base, s).first; - container_ty = *container_ty.pointee; - } else - container_addr = lower_place_address(*index_expr.base, s); - if (container_ty.kind != TypeKind::Named || - (container_ty.name != "Slice" && container_ty.name != "SliceMut") || - container_ty.args.empty()) { - diags_.error(e.range, "E0505", "MIR indexing requires Slice or SliceMut"); - return "null"; - } - const Type elem = container_ty.args[0]; - const bool mut = container_ty.name == "SliceMut"; - const Type data_ty = Type::raw_ptr(elem, mut); - const Type usize_ty = Type::builtin("usize"); - std::string data_addr = temp(s); - mir::Instruction data_field; - data_field.op = mir::Op::FieldAddr; - data_field.result = data_addr; - data_field.type = data_ty; - data_field.args = {container_addr}; - data_field.text = "0"; - data_field.auxiliary_type = container_ty; - data_field.range = e.range; - emit(s, std::move(data_field)); - std::string data = temp(s); - emit(s, {mir::Op::Load, data, data_ty, {data_addr}, "", {}, false, e.range}); - std::string len_addr = temp(s); - mir::Instruction len_field; - len_field.op = mir::Op::FieldAddr; - len_field.result = len_addr; - len_field.type = usize_ty; - len_field.args = {container_addr}; - len_field.text = "1"; - len_field.auxiliary_type = container_ty; - len_field.range = e.range; - emit(s, std::move(len_field)); - std::string len = temp(s); - emit(s, {mir::Op::Load, len, usize_ty, {len_addr}, "", {}, false, e.range}); - auto [index_value, index_ty] = lower_expr(*index_expr.index, s); - std::string result = temp(s); - mir::Instruction indexed; - indexed.op = mir::Op::IndexAddr; - indexed.result = result; - indexed.type = elem; - indexed.args = {data, index_value, len}; - indexed.auxiliary_type = index_ty; - indexed.range = e.range; - emit(s, std::move(indexed)); - return result; - } - diags_.error(e.range, "E0504", "expression is not an addressable place"); - return "null"; -} -std::string -MIRLowerer::specialize_call(const ast::CallExpr& c, const FunctionSig& sig, FnState& s) { - if (sig.generic_names.empty()) - return sig.name; - std::unordered_map subst; - if (!c.generic_args.empty()) { - for (std::size_t i = 0; i < std::min(c.generic_args.size(), sig.generic_names.size()); ++i) - subst[sig.generic_names[i]] = type_from_ast(c.generic_args[i]); - } else { - for (std::size_t i = 0; i < c.args.size() && i < sig.params.size(); ++i) { - Type formal = subst_type(sig.params[i], s), actual = expr_type(*c.args[i], s); - if (formal.kind == TypeKind::Generic) - subst[formal.name] = actual; - } - } - std::ostringstream name; - name << sig.name; - for (const auto& g : sig.generic_names) { - auto it = subst.find(g); - if (it == subst.end()) - continue; - name << '$'; - for (char ch : it->second.str()) - name << (std::isalnum(static_cast(ch)) ? ch : '_'); - } - std::string n = name.str(); - if (!emitted_.contains(n)) { - pending_.push_back({sig.decl, subst, n}); - } - return n; -} -std::pair MIRLowerer::lower_expr(const ast::Expr& e, FnState& s) { - Type ty = expr_type(e, s); - switch (e.kind) { - case ast::ExprKind::Integer: { - auto& x = static_cast(e); - std::string r = temp(s); - emit(s, {mir::Op::ConstInt, r, ty, {}, x.value, {}, false, e.range}); - return {r, ty}; - } - case ast::ExprKind::Float: { - auto& x = static_cast(e); - std::string r = temp(s); - emit(s, {mir::Op::ConstFloat, r, ty, {}, x.value, {}, false, e.range}); - return {r, ty}; - } - case ast::ExprKind::Bool: { - auto& x = static_cast(e); - std::string r = temp(s); - emit(s, {mir::Op::ConstBool, r, ty, {}, x.value ? "1" : "0", {}, false, e.range}); - return {r, ty}; - } - case ast::ExprKind::Char: { - auto& x = static_cast(e); - std::string r = temp(s); - std::uint32_t v = x.value.empty() ? 0 : static_cast(x.value[0]); - emit(s, {mir::Op::ConstChar, r, ty, {}, std::to_string(v), {}, false, e.range}); - return {r, ty}; - } - case ast::ExprKind::String: { - auto& x = static_cast(e); - std::string r = temp(s), sym = intern_string(x.value); - emit(s, {mir::Op::ConstString, r, ty, {}, sym, {}, false, e.range}); - return {r, ty}; - } - case ast::ExprKind::Name: { - auto addr = lower_place_address(e, s); - std::string r = temp(s); - const bool atomic = place_is_atomic(e, s); - emit(s, - {mir::Op::Load, - r, - ty, - {addr}, - atomic ? load_order(e, s) : "", - {}, - atomic, - e.range}); - return {r, ty}; - } - case ast::ExprKind::StructLiteral: { - auto& st = static_cast(e); - auto si = model_.structs.find(st.name); - if (si == model_.structs.end() || !si->second.decl) { - diags_.error(e.range, "E0506", "missing struct layout for '" + st.name + "'"); - return {"undef", ty}; - } - std::unordered_map values; - for (const auto& f : st.fields) - values[f.name] = f.value.get(); - // Expected field types under the call-site substitution, so - // integer literals coerce across widths exactly like call - // arguments and `let` bindings do (sema permits them via - // `can_coerce_expr`; the backend must never see mismatched - // operand types, e.g. i32 `7` into a u64 generic field). - std::unordered_set field_generics(si->second.generic_names.begin(), - si->second.generic_names.end()); - std::unordered_map field_subst; - for (std::size_t i = 0; i < si->second.generic_names.size() && i < ty.args.size(); ++i) - field_subst[si->second.generic_names[i]] = ty.args[i]; - std::vector args; - for (const auto& decl_field : si->second.decl->fields) { - auto it = values.find(decl_field.name); - if (it == values.end()) { - args.push_back("undef"); - continue; - } - auto [field_value, field_actual] = lower_expr(*it->second, s); - const Type field_expected = - substitute_type(type_from_ast(decl_field.type, field_generics), field_subst); - args.push_back( - coerce_value(s, field_value, field_actual, field_expected, it->second->range)); - } - std::string r = temp(s); - emit(s, {mir::Op::StructInit, r, ty, std::move(args), "", {}, false, e.range}); - return {r, ty}; - } - case ast::ExprKind::Borrow: { - auto& b = static_cast(e); - std::string a = lower_place_address(*b.target, s); - std::string r = temp(s); - emit(s, {mir::Op::AddressOf, r, ty, {a}, "", {}, false, e.range}); - return {r, ty}; - } - case ast::ExprKind::Unary: { - auto& u = static_cast(e); - auto [v, t] = lower_expr(*u.operand, s); - if (u.op == "move") - return {v, ty}; - if (u.op == "*") { - std::string r = temp(s); - emit(s, {mir::Op::Load, r, ty, {v}, "", {}, false, e.range}); - return {r, ty}; - } - std::string r = temp(s); - std::string zero = temp(s); - if (u.op == "-") { - // `ty` is the (possibly coerced) result type from sema, while `t` - // is the inner operand's own type. For `-literal` coerced to a - // wider integer (e.g. `result >= -4095` with result: i64), using - // `t` here emits `sub i32` whose result is later used as i64 and - // produces invalid `icmp sge i64 %x, %y(i32)` IR at -O0 - // (-O2 hides it via constant folding). Lower in `ty` instead, - // casting the operand up when needed. - std::string operand = v; - if (t != ty) { - std::string casted = temp(s); - emit(s, {mir::Op::Cast, casted, ty, {v}, t.str(), {}, false, e.range}); - operand = casted; - } - if (ty.is_float()) { - emit(s, {mir::Op::ConstFloat, zero, ty, {}, "0.0", {}, false, e.range}); - } else { - emit(s, {mir::Op::ConstInt, zero, ty, {}, "0", {}, false, e.range}); - } - emit(s, {mir::Op::Binary, r, ty, {zero, operand}, "-", {}, false, e.range}); - } else { - std::string operand = v; - if (t != ty) { - std::string casted = temp(s); - emit(s, {mir::Op::Cast, casted, ty, {v}, t.str(), {}, false, e.range}); - operand = casted; - } - emit(s, {mir::Op::Binary, r, ty, {operand}, u.op, {}, false, e.range}); - } - return {r, ty}; - } - case ast::ExprKind::Binary: { - auto& b = static_cast(e); - auto [l, lt] = lower_expr(*b.lhs, s); - auto [r, rt] = lower_expr(*b.rhs, s); - std::string out = temp(s); - bool cmp = b.op == "==" || b.op == "!=" || b.op == "<" || b.op == "<=" || b.op == ">" || - b.op == ">="; - if (cmp) { - // Comparison operands must share one LLVM integer type. - l = coerce_value(s, l, lt, lt, b.lhs->range); - r = coerce_value(s, r, rt, lt, b.rhs->range); - } else { - l = coerce_value(s, l, lt, ty, b.lhs->range); - r = coerce_value(s, r, rt, ty, b.rhs->range); - } - emit(s, - {cmp ? mir::Op::Compare : mir::Op::Binary, - out, - cmp ? lt : ty, - {l, r}, - b.op, - {}, - false, - e.range}); - return {out, ty}; - } - case ast::ExprKind::Call: { - auto& c = static_cast(e); - if (c.callee->kind == ast::ExprKind::Name) { - const auto& name = static_cast(*c.callee).name; - if (builtin_function(name)) - return lower_builtin_call(c, s); - } - std::string callee; - std::vector args; - if (c.callee->kind == ast::ExprKind::Name) { - auto& n = static_cast(*c.callee); - auto fi = model_.functions.find(n.name); - callee = n.name; - if (fi != model_.functions.end()) - callee = specialize_call(c, fi->second, s); - for (std::size_t i = 0; i < c.args.size(); ++i) { - auto [v, actual] = lower_expr(*c.args[i], s); - if (fi != model_.functions.end() && i < fi->second.params.size()) { - Type expected = subst_type(fi->second.params[i], s); - // For generic callees the formal may still be generic; - // coerce_value is a no-op unless it is a concrete - // int/bool/float mismatch (e.g. i32 literal -> i64 param). - v = coerce_value(s, v, actual, expected, c.args[i]->range); - } - args.push_back(v); - } - } else if (c.callee->kind == ast::ExprKind::Member) { - auto& member = static_cast(*c.callee); - Type receiver_ty = expr_type(*member.base, s); - Type owner = receiver_ty; - if (owner.kind == TypeKind::Ref && owner.pointee) - owner = *owner.pointee; - const auto fi = model_.functions.find(owner.name + "::" + member.member); - if (fi == model_.functions.end()) { - diags_.error( - e.range, "E0507", "internal: method signature missing during MIR lowering"); - return {"undef", ty}; - } - callee = fi->second.name; - if (!fi->second.params.empty() && fi->second.params.front().kind == TypeKind::Ref) { - if (receiver_ty.kind == TypeKind::Ref) - args.push_back(lower_expr(*member.base, s).first); - else - args.push_back(lower_place_address(*member.base, s)); - } else - args.push_back(lower_expr(*member.base, s).first); - // Method params[0] is the receiver already handled above. - const std::size_t param_offset = - fi->second.params.empty() ? 0 : args.size() - c.args.size(); - for (std::size_t i = 0; i < c.args.size(); ++i) { - auto [v, actual] = lower_expr(*c.args[i], s); - if (param_offset + i < fi->second.params.size()) { - Type expected = subst_type(fi->second.params[param_offset + i], s); - v = coerce_value(s, v, actual, expected, c.args[i]->range); - } - args.push_back(v); - } - } else { - diags_.error( - e.range, "E0507", "internal: unsupported call target during MIR lowering"); - return {"undef", ty}; - } - std::string r = ty.is_unit() ? "" : temp(s); - emit(s, {mir::Op::Call, r, ty, std::move(args), callee, {}, false, e.range}); - return {r, ty}; - } - case ast::ExprKind::Cast: { - auto& c = static_cast(e); - auto [v, from] = lower_expr(*c.value, s); - std::string r = temp(s); - emit(s, {mir::Op::Cast, r, ty, {v}, from.str(), {}, false, e.range}); - return {r, ty}; - } - case ast::ExprKind::Asm: { - auto& a = static_cast(e); - mir::Instruction in; - in.op = mir::Op::InlineAsm; - in.type = Type::builtin("unit"); - in.text = a.assembly; - in.flag = a.is_volatile; - in.range = e.range; - for (const auto& o : a.operands) { - mir::AsmOperand mo; - mo.kind = o.kind; - mo.constraint = o.constraint; - if (o.value) { - auto [v, t] = lower_expr(*o.value, s); - mo.value = v; - mo.type = t; - } - if (!o.out_name.empty()) { - auto it = s.slots.find(o.out_name); - if (it != s.slots.end()) { - mo.out_slot = it->second; - mo.type = s.local_types[o.out_name]; - } - } - in.asm_operands.push_back(std::move(mo)); - } - emit(s, std::move(in)); - return {"", ty}; - } - case ast::ExprKind::Await: { - auto& a = static_cast(e); - auto [v, future_ty] = lower_expr(*a.value, s); - (void)future_ty; - std::string r = temp(s); - emit(s, {mir::Op::Await, r, ty, {v}, "", {}, false, e.range}); - return {r, ty}; - } - case ast::ExprKind::Member: { - std::string addr = lower_place_address(e, s); - std::string r = temp(s); - const bool atomic = place_is_atomic(e, s); - emit(s, - {mir::Op::Load, - r, - ty, - {addr}, - atomic ? load_order(e, s) : "", - {}, - atomic, - e.range}); - return {r, ty}; - } - case ast::ExprKind::Index: { - std::string addr = lower_place_address(e, s); - std::string r = temp(s); - emit(s, {mir::Op::Load, r, ty, {addr}, "", {}, false, e.range}); - return {r, ty}; - } - } - return {"undef", ty}; -} - -std::pair MIRLowerer::lower_builtin_call(const ast::CallExpr& call, FnState& s) { - const auto& name = static_cast(*call.callee).name; - const Type result = expr_type(call, s); - auto declare_call = [&](const std::string& runtime_name, - const Type& runtime_result, - const std::vector& parameter_types) { - std::vector parameters; - for (std::size_t i = 0; i < parameter_types.size(); ++i) - parameters.push_back({"p" + std::to_string(i), parameter_types[i]}); - declare_runtime_function(runtime_name, runtime_result, std::move(parameters)); - }; - auto runtime_call = [&](const std::string& runtime_name, - const Type& runtime_result, - const std::vector& parameter_types, - std::vector arguments, - const SourceRange& range) { - declare_call(runtime_name, runtime_result, parameter_types); - const std::string output = runtime_result.is_unit() ? "" : temp(s); - emit(s, - {mir::Op::Call, - output, - runtime_result, - std::move(arguments), - runtime_name, - {}, - false, - range}); - return output; - }; - auto cast_value = - [&](const std::string& value, const Type& from, const Type& to, const SourceRange& range) { - if (from == to) - return value; - const std::string output = temp(s); - emit(s, {mir::Op::Cast, output, to, {value}, from.str(), {}, false, range}); - return output; - }; - - if (name == "print" || name == "println" || name == "eprint" || name == "eprintln") { - const bool stderr_output = name == "eprint" || name == "eprintln"; - const Type i32 = Type::builtin("i32"); - const Type fd_type = i32; - const std::string fd = temp(s); - emit( - s, - {mir::Op::ConstInt, fd, fd_type, {}, stderr_output ? "2" : "1", {}, false, call.range}); - - for (std::size_t i = 0; i < call.args.size(); ++i) { - if (i != 0) { - runtime_call( - "uinx_print_space", Type::builtin("unit"), {fd_type}, {fd}, call.range); - } - auto [value, type] = lower_expr(*call.args[i], s); - if (type.is_integer() && type.kind != TypeKind::I128 && type.kind != TypeKind::U128) { - const Type target = Type::builtin(unsigned_integer_kind(type.kind) ? "u64" : "i64"); - value = cast_value(value, type, target, call.args[i]->range); - runtime_call(unsigned_integer_kind(type.kind) ? "uinx_print_u64" : "uinx_print_i64", - Type::builtin("unit"), - {target, fd_type}, - {value, fd}, - call.args[i]->range); - } else if (type.is_float()) { - value = cast_value(value, type, Type::builtin("f64"), call.args[i]->range); - runtime_call("uinx_print_f64", - Type::builtin("unit"), - {Type::builtin("f64"), fd_type}, - {value, fd}, - call.args[i]->range); - } else if (type.kind == TypeKind::Bool) { - value = cast_value(value, type, Type::builtin("i64"), call.args[i]->range); - runtime_call("uinx_print_bool", - Type::builtin("unit"), - {Type::builtin("i64"), fd_type}, - {value, fd}, - call.args[i]->range); - } else if (type.kind == TypeKind::Char) { - runtime_call("uinx_print_char", - Type::builtin("unit"), - {Type::builtin("i32"), fd_type}, - {value, fd}, - call.args[i]->range); - } else { - runtime_call("uinx_print_cstr", - Type::builtin("unit"), - {Type::ref(Type::builtin("str"), false), fd_type}, - {value, fd}, - call.args[i]->range); - } - } - runtime_call("uinx_print_newline", Type::builtin("unit"), {fd_type}, {fd}, call.range); - return {"", Type::builtin("unit")}; - } - - if (name == "input") { - const Type str_ref = Type::ref(Type::builtin("str"), false); - std::string prompt = "null"; - if (!call.args.empty()) - prompt = lower_expr(*call.args[0], s).first; - const std::string fd = temp(s); - emit(s, {mir::Op::ConstInt, fd, Type::builtin("i32"), {}, "1", {}, false, call.range}); - return {runtime_call("uinx_input_cstr", - str_ref, - {str_ref, Type::builtin("i32")}, - {prompt, fd}, - call.range), - str_ref}; - } - - auto [value, type] = lower_expr(*call.args[0], s); - const Type str_ref = Type::ref(Type::builtin("str"), false); - const auto is_string = [](const Type& checked) { - return checked.kind == TypeKind::Str || (checked.kind == TypeKind::Ref && checked.pointee && - checked.pointee->kind == TypeKind::Str); - }; - if (name == "str") { - if (is_string(type)) - return {value, str_ref}; - if (type.is_integer()) { - const Type target = Type::builtin(unsigned_integer_kind(type.kind) ? "u64" : "i64"); - value = cast_value(value, type, target, call.range); - return {runtime_call(unsigned_integer_kind(type.kind) ? "uinx_string_from_u64" - : "uinx_string_from_i64", - str_ref, - {target}, - {value}, - call.range), - str_ref}; - } - if (type.is_float()) { - value = cast_value(value, type, Type::builtin("f64"), call.range); - return { - runtime_call( - "uinx_string_from_f64", str_ref, {Type::builtin("f64")}, {value}, call.range), - str_ref}; - } - if (type.kind == TypeKind::Bool) { - value = cast_value(value, type, Type::builtin("i64"), call.range); - return { - runtime_call( - "uinx_string_from_bool", str_ref, {Type::builtin("i64")}, {value}, call.range), - str_ref}; - } - if (type.kind == TypeKind::Char) - return { - runtime_call( - "uinx_string_from_char", str_ref, {Type::builtin("i32")}, {value}, call.range), - str_ref}; - } - if (name == "int" || name == "float") { - const Type target = Type::builtin(name == "int" ? "i64" : "f64"); - if (is_string(type)) { - return {runtime_call(name == "int" ? "uinx_parse_i64_cstr" : "uinx_parse_f64_cstr", - target, - {str_ref}, - {value}, - call.range), - target}; - } - return {cast_value(value, type, target, call.range), target}; - } - if (name == "bool") { - if (type.kind == TypeKind::Bool) - return {value, type}; - if (is_string(type)) { - const Type i64 = Type::builtin("i64"); - value = runtime_call("uinx_parse_i64_cstr", i64, {str_ref}, {value}, call.range); - type = i64; - } - const std::string zero = temp(s); - emit(s, {mir::Op::ConstInt, zero, type, {}, "0", {}, false, call.range}); - const std::string output = temp(s); - emit(s, - {mir::Op::Compare, - output, - Type::builtin("bool"), - {value, zero}, - "!=", - {}, - false, - call.range}); - return {output, Type::builtin("bool")}; - } - if (name == "len") { - if (is_string(type)) - return {runtime_call( - "uinx_cstr_len", Type::builtin("usize"), {str_ref}, {value}, call.range), - Type::builtin("usize")}; - } - diags_.error(call.range, "E0507", "internal: unsupported builtin lowering"); - return {"undef", result}; -} -void MIROptimizer::remove_unreachable(mir::Function& f) const { - std::unordered_set reachable; - if (f.blocks.empty()) - return; - std::unordered_map labels; - for (std::size_t i = 0; i < f.blocks.size(); ++i) - labels[f.blocks[i].label] = i; - std::vector work{0}; - while (!work.empty()) { - const std::size_t index = work.back(); - work.pop_back(); - if (index >= f.blocks.size() || !reachable.insert(f.blocks[index].label).second) - continue; - const auto& block = f.blocks[index]; - if (block.instructions.empty()) { - if (index + 1 < f.blocks.size()) - work.push_back(index + 1); - continue; - } - const auto& tail = block.instructions.back(); - if (tail.op == mir::Op::Jump && !tail.args.empty()) { - if (auto it = labels.find(tail.args[0]); it != labels.end()) - work.push_back(it->second); - } else if (tail.op == mir::Op::CondJump && tail.args.size() >= 3) { - if (auto it = labels.find(tail.args[1]); it != labels.end()) - work.push_back(it->second); - if (auto it = labels.find(tail.args[2]); it != labels.end()) - work.push_back(it->second); - } else if (tail.op != mir::Op::Return && index + 1 < f.blocks.size()) { - work.push_back(index + 1); - } - } - f.blocks.erase(std::remove_if(f.blocks.begin(), - f.blocks.end(), - [&](const mir::BasicBlock& block) { - return !reachable.contains(block.label); - }), - f.blocks.end()); -} - -void MIROptimizer::forward_local_loads(mir::Function& fn) const { - for (auto& block : fn.blocks) { - std::unordered_map slots; - std::unordered_map aliases; - auto resolve = [&](std::string value) { - for (int depth = 0; depth < 32; ++depth) { - auto it = aliases.find(value); - if (it == aliases.end() || it->second == value) - break; - value = it->second; - } - return value; - }; - std::vector output; - output.reserve(block.instructions.size()); - for (auto instruction : block.instructions) { - for (auto& arg : instruction.args) - arg = resolve(arg); - for (auto& operand : instruction.asm_operands) { - if (!operand.value.empty()) - operand.value = resolve(operand.value); - } - const auto is_local_slot = [](const std::string& address) { - return !address.empty() && address.front() == '%' && - address.find(".slot.") != std::string::npos; - }; - if (instruction.op == mir::Op::Load && !instruction.flag && !instruction.args.empty()) { - const auto it = slots.find(instruction.args[0]); - if (it != slots.end()) { - aliases[instruction.result] = resolve(it->second); - continue; - } - } - if (instruction.op == mir::Op::Store && !instruction.flag && - instruction.args.size() >= 2) { - const std::string& address = instruction.args[1]; - if (is_local_slot(address)) - slots[address] = instruction.args[0]; - else - slots.clear(); - } else if (instruction.op == mir::Op::AddressOf) { - if (!instruction.args.empty()) - slots.erase(instruction.args[0]); - } else if (instruction.op == mir::Op::Call || instruction.op == mir::Op::InlineAsm || - instruction.op == mir::Op::AtomicRmw || instruction.op == mir::Op::Fence || - instruction.op == mir::Op::CompilerFence || - (instruction.op == mir::Op::Load && instruction.flag) || - (instruction.op == mir::Op::Store && instruction.flag)) { - slots.clear(); - } - output.push_back(std::move(instruction)); - } - block.instructions = std::move(output); - } -} - -void MIROptimizer::constant_fold(mir::Function& fn) const { - for (auto& block : fn.blocks) { - std::unordered_map constants; - auto get = [&](const std::string& value, std::int64_t& out) { - if (auto it = constants.find(value); it != constants.end()) { - out = it->second; - return true; - } - char* end = nullptr; - const long long parsed = std::strtoll(value.c_str(), &end, 0); - if (end && *end == '\0' && end != value.c_str()) { - out = static_cast(parsed); - return true; - } - return false; - }; - for (auto& instruction : block.instructions) { - if ((instruction.op == mir::Op::ConstInt || instruction.op == mir::Op::ConstBool || - instruction.op == mir::Op::ConstChar) && - !instruction.result.empty()) { - std::int64_t value{}; - if (get(instruction.text, value)) - constants[instruction.result] = value; - continue; - } - if (instruction.type.kind == TypeKind::I128 || instruction.type.kind == TypeKind::U128) - continue; - if (instruction.op == mir::Op::Binary && !instruction.result.empty()) { - std::int64_t a{}, b{}; - if (instruction.text == "!" && instruction.args.size() == 1 && - get(instruction.args[0], a)) { - const std::int64_t result = instruction.type.kind == TypeKind::Bool ? !a : ~a; - instruction.op = instruction.type.kind == TypeKind::Bool ? mir::Op::ConstBool - : mir::Op::ConstInt; - instruction.args.clear(); - instruction.text = std::to_string(result); - constants[instruction.result] = result; - continue; - } - if (instruction.args.size() != 2 || !get(instruction.args[0], a) || - !get(instruction.args[1], b)) - continue; - std::int64_t result{}; - bool ok = true; - if (instruction.text == "+") - result = static_cast(static_cast(a) + - static_cast(b)); - else if (instruction.text == "-") - result = static_cast(static_cast(a) - - static_cast(b)); - else if (instruction.text == "*") - result = static_cast(static_cast(a) * - static_cast(b)); - else if (instruction.text == "/") { - if (b == 0) - ok = false; - else - result = unsigned_integer_kind(instruction.type.kind) - ? static_cast(static_cast(a) / - static_cast(b)) - : a / b; - } else if (instruction.text == "%") { - if (b == 0) - ok = false; - else - result = unsigned_integer_kind(instruction.type.kind) - ? static_cast(static_cast(a) % - static_cast(b)) - : a % b; - } else if (instruction.text == "&" || instruction.text == "&&") - result = a & b; - else if (instruction.text == "|" || instruction.text == "||") - result = a | b; - else if (instruction.text == "^") - result = a ^ b; - else if (instruction.text == "<<") { - if (b < 0 || b >= 64) - ok = false; - else - result = static_cast(static_cast(a) << b); - } else if (instruction.text == ">>") { - if (b < 0 || b >= 64) - ok = false; - else - result = unsigned_integer_kind(instruction.type.kind) - ? static_cast(static_cast(a) >> b) - : (a >> b); - } else - ok = false; - if (ok) { - instruction.op = mir::Op::ConstInt; - instruction.args.clear(); - instruction.text = std::to_string(result); - constants[instruction.result] = result; - } - } else if (instruction.op == mir::Op::Compare && instruction.args.size() == 2 && - !instruction.result.empty()) { - std::int64_t a{}, b{}; - if (!get(instruction.args[0], a) || !get(instruction.args[1], b)) - continue; - const bool u = unsigned_integer_kind(instruction.type.kind); - bool result = false; - if (instruction.text == "==") - result = a == b; - else if (instruction.text == "!=") - result = a != b; - else if (instruction.text == "<") - result = - u ? static_cast(a) < static_cast(b) : a < b; - else if (instruction.text == "<=") - result = - u ? static_cast(a) <= static_cast(b) : a <= b; - else if (instruction.text == ">") - result = - u ? static_cast(a) > static_cast(b) : a > b; - else if (instruction.text == ">=") - result = - u ? static_cast(a) >= static_cast(b) : a >= b; - instruction.op = mir::Op::ConstBool; - instruction.type = Type::builtin("bool"); - instruction.args.clear(); - instruction.text = result ? "1" : "0"; - constants[instruction.result] = result ? 1 : 0; - } - } - } -} - -void MIROptimizer::eliminate_dead_values(mir::Function& fn) const { - bool changed = true; - while (changed) { - changed = false; - std::unordered_map uses; - for (const auto& block : fn.blocks) - for (const auto& instruction : block.instructions) { - for (const auto& arg : instruction.args) - ++uses[arg]; - for (const auto& operand : instruction.asm_operands) - if (!operand.value.empty()) - ++uses[operand.value]; - } - for (auto& block : fn.blocks) { - auto it = std::remove_if( - block.instructions.begin(), - block.instructions.end(), - [&](const mir::Instruction& instruction) { - if (instruction.result.empty() || uses[instruction.result] != 0) - return false; - bool pure = - instruction.op == mir::Op::Alloca || instruction.op == mir::Op::ConstInt || - instruction.op == mir::Op::ConstFloat || - instruction.op == mir::Op::ConstBool || - instruction.op == mir::Op::ConstChar || - instruction.op == mir::Op::ConstString || - instruction.op == mir::Op::StructInit || - instruction.op == mir::Op::FieldAddr || instruction.op == mir::Op::Binary || - instruction.op == mir::Op::Compare || instruction.op == mir::Op::Cast || - instruction.op == mir::Op::AddressOf || - (instruction.op == mir::Op::Load && !instruction.flag); - if (pure) - changed = true; - return pure; - }); - block.instructions.erase(it, block.instructions.end()); - } - } -} - -void MIROptimizer::run(mir::Module& m) const { - if (level_ <= 0) - return; - for (auto& f : m.functions) { - if (f.is_extern) - continue; - remove_unreachable(f); - forward_local_loads(f); - constant_fold(f); - eliminate_dead_values(f); - if (level_ >= 2) { - forward_local_loads(f); - eliminate_dead_values(f); - } - } -} - } // namespace uinx diff --git a/src/mir_expr.cpp b/src/mir_expr.cpp new file mode 100644 index 0000000..ccb85fe --- /dev/null +++ b/src/mir_expr.cpp @@ -0,0 +1,656 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "mir_internal.hpp" +#include "uinx/mir.hpp" + +#include +#include +#include +#include +#include +#include + +namespace uinx { +using namespace mir_detail; + +std::string MIRLowerer::lower_place_address(const ast::Expr& e, FnState& s) { + if (e.kind == ast::ExprKind::Unary) { + const auto& u = static_cast(e); + if (u.op == "*") + return lower_expr(*u.operand, s).first; + } + if (e.kind == ast::ExprKind::Name) { + auto& n = static_cast(e); + auto it = s.slots.find(n.name); + if (it != s.slots.end()) + return it->second; + if (model_.globals.contains(n.name)) + return "@" + n.name; + diags_.error(e.range, "E0502", "internal: no storage for '" + n.name + "'"); + return "null"; + } + if (e.kind == ast::ExprKind::Member) { + const auto& m = static_cast(e); + Type base_ty = expr_type(*m.base, s); + std::string base_addr; + if (base_ty.kind == TypeKind::Ref && base_ty.pointee) { + auto lowered = lower_expr(*m.base, s); + base_addr = lowered.first; + base_ty = *base_ty.pointee; + } else + base_addr = lower_place_address(*m.base, s); + if (base_ty.kind != TypeKind::Named) { + diags_.error(e.range, "E0503", "field address base is not a struct"); + return "null"; + } + auto si = model_.structs.find(base_ty.name); + if (si == model_.structs.end() || !si->second.decl) { + diags_.error(e.range, "E0503", "unknown struct layout for '" + base_ty.name + "'"); + return "null"; + } + std::size_t index = 0; + bool found = false; + for (const auto& field : si->second.decl->fields) { + if (field.name == m.member) { + found = true; + break; + } + ++index; + } + if (!found) { + diags_.error(e.range, "E0503", "unknown field '" + m.member + "'"); + return "null"; + } + Type field_ty = expr_type(e, s); + std::string result = temp(s); + mir::Instruction in; + in.op = mir::Op::FieldAddr; + in.result = result; + in.type = field_ty; + in.args = {base_addr}; + in.text = std::to_string(index); + in.range = e.range; + in.auxiliary_type = base_ty; + emit(s, std::move(in)); + return result; + } + if (e.kind == ast::ExprKind::Index) { + const auto& index_expr = static_cast(e); + Type container_ty = expr_type(*index_expr.base, s); + std::string container_addr; + if (container_ty.kind == TypeKind::Ref && container_ty.pointee) { + container_addr = lower_expr(*index_expr.base, s).first; + container_ty = *container_ty.pointee; + } else + container_addr = lower_place_address(*index_expr.base, s); + if (container_ty.kind != TypeKind::Named || + (container_ty.name != "Slice" && container_ty.name != "SliceMut") || + container_ty.args.empty()) { + diags_.error(e.range, "E0505", "MIR indexing requires Slice or SliceMut"); + return "null"; + } + const Type elem = container_ty.args[0]; + const bool mut = container_ty.name == "SliceMut"; + const Type data_ty = Type::raw_ptr(elem, mut); + const Type usize_ty = Type::builtin("usize"); + std::string data_addr = temp(s); + mir::Instruction data_field; + data_field.op = mir::Op::FieldAddr; + data_field.result = data_addr; + data_field.type = data_ty; + data_field.args = {container_addr}; + data_field.text = "0"; + data_field.auxiliary_type = container_ty; + data_field.range = e.range; + emit(s, std::move(data_field)); + std::string data = temp(s); + emit(s, {mir::Op::Load, data, data_ty, {data_addr}, "", {}, false, e.range}); + std::string len_addr = temp(s); + mir::Instruction len_field; + len_field.op = mir::Op::FieldAddr; + len_field.result = len_addr; + len_field.type = usize_ty; + len_field.args = {container_addr}; + len_field.text = "1"; + len_field.auxiliary_type = container_ty; + len_field.range = e.range; + emit(s, std::move(len_field)); + std::string len = temp(s); + emit(s, {mir::Op::Load, len, usize_ty, {len_addr}, "", {}, false, e.range}); + auto [index_value, index_ty] = lower_expr(*index_expr.index, s); + std::string result = temp(s); + mir::Instruction indexed; + indexed.op = mir::Op::IndexAddr; + indexed.result = result; + indexed.type = elem; + indexed.args = {data, index_value, len}; + indexed.auxiliary_type = index_ty; + indexed.range = e.range; + emit(s, std::move(indexed)); + return result; + } + diags_.error(e.range, "E0504", "expression is not an addressable place"); + return "null"; +} +std::string +MIRLowerer::specialize_call(const ast::CallExpr& c, const FunctionSig& sig, FnState& s) { + if (sig.generic_names.empty()) + return sig.name; + std::unordered_map subst; + if (!c.generic_args.empty()) { + for (std::size_t i = 0; i < std::min(c.generic_args.size(), sig.generic_names.size()); ++i) + subst[sig.generic_names[i]] = type_from_ast(c.generic_args[i]); + } else { + for (std::size_t i = 0; i < c.args.size() && i < sig.params.size(); ++i) { + Type formal = subst_type(sig.params[i], s), actual = expr_type(*c.args[i], s); + if (formal.kind == TypeKind::Generic) + subst[formal.name] = actual; + } + } + std::ostringstream name; + name << sig.name; + for (const auto& g : sig.generic_names) { + auto it = subst.find(g); + if (it == subst.end()) + continue; + name << '$'; + for (char ch : it->second.str()) + name << (std::isalnum(static_cast(ch)) ? ch : '_'); + } + std::string n = name.str(); + if (!emitted_.contains(n)) { + pending_.push_back({sig.decl, subst, n}); + } + return n; +} +std::pair MIRLowerer::lower_expr(const ast::Expr& e, FnState& s) { + Type ty = expr_type(e, s); + switch (e.kind) { + case ast::ExprKind::Integer: { + auto& x = static_cast(e); + std::string r = temp(s); + emit(s, {mir::Op::ConstInt, r, ty, {}, x.value, {}, false, e.range}); + return {r, ty}; + } + case ast::ExprKind::Float: { + auto& x = static_cast(e); + std::string r = temp(s); + emit(s, {mir::Op::ConstFloat, r, ty, {}, x.value, {}, false, e.range}); + return {r, ty}; + } + case ast::ExprKind::Bool: { + auto& x = static_cast(e); + std::string r = temp(s); + emit(s, {mir::Op::ConstBool, r, ty, {}, x.value ? "1" : "0", {}, false, e.range}); + return {r, ty}; + } + case ast::ExprKind::Char: { + auto& x = static_cast(e); + std::string r = temp(s); + std::uint32_t v = x.value.empty() ? 0 : static_cast(x.value[0]); + emit(s, {mir::Op::ConstChar, r, ty, {}, std::to_string(v), {}, false, e.range}); + return {r, ty}; + } + case ast::ExprKind::String: { + auto& x = static_cast(e); + std::string r = temp(s), sym = intern_string(x.value); + emit(s, {mir::Op::ConstString, r, ty, {}, sym, {}, false, e.range}); + return {r, ty}; + } + case ast::ExprKind::Name: { + auto addr = lower_place_address(e, s); + std::string r = temp(s); + const bool atomic = place_is_atomic(e, s); + emit(s, + {mir::Op::Load, + r, + ty, + {addr}, + atomic ? load_order(e, s) : "", + {}, + atomic, + e.range}); + return {r, ty}; + } + case ast::ExprKind::StructLiteral: { + auto& st = static_cast(e); + auto si = model_.structs.find(st.name); + if (si == model_.structs.end() || !si->second.decl) { + diags_.error(e.range, "E0506", "missing struct layout for '" + st.name + "'"); + return {"undef", ty}; + } + std::unordered_map values; + for (const auto& f : st.fields) + values[f.name] = f.value.get(); + // Expected field types under the call-site substitution, so + // integer literals coerce across widths exactly like call + // arguments and `let` bindings do (sema permits them via + // `can_coerce_expr`; the backend must never see mismatched + // operand types, e.g. i32 `7` into a u64 generic field). + std::unordered_set field_generics(si->second.generic_names.begin(), + si->second.generic_names.end()); + std::unordered_map field_subst; + for (std::size_t i = 0; i < si->second.generic_names.size() && i < ty.args.size(); ++i) + field_subst[si->second.generic_names[i]] = ty.args[i]; + std::vector args; + for (const auto& decl_field : si->second.decl->fields) { + auto it = values.find(decl_field.name); + if (it == values.end()) { + args.push_back("undef"); + continue; + } + auto [field_value, field_actual] = lower_expr(*it->second, s); + const Type field_expected = + substitute_type(type_from_ast(decl_field.type, field_generics), field_subst); + args.push_back( + coerce_value(s, field_value, field_actual, field_expected, it->second->range)); + } + std::string r = temp(s); + emit(s, {mir::Op::StructInit, r, ty, std::move(args), "", {}, false, e.range}); + return {r, ty}; + } + case ast::ExprKind::Borrow: { + auto& b = static_cast(e); + std::string a = lower_place_address(*b.target, s); + std::string r = temp(s); + emit(s, {mir::Op::AddressOf, r, ty, {a}, "", {}, false, e.range}); + return {r, ty}; + } + case ast::ExprKind::Unary: { + auto& u = static_cast(e); + auto [v, t] = lower_expr(*u.operand, s); + if (u.op == "move") + return {v, ty}; + if (u.op == "*") { + std::string r = temp(s); + emit(s, {mir::Op::Load, r, ty, {v}, "", {}, false, e.range}); + return {r, ty}; + } + std::string r = temp(s); + std::string zero = temp(s); + if (u.op == "-") { + // `ty` is the (possibly coerced) result type from sema, while `t` + // is the inner operand's own type. For `-literal` coerced to a + // wider integer (e.g. `result >= -4095` with result: i64), using + // `t` here emits `sub i32` whose result is later used as i64 and + // produces invalid `icmp sge i64 %x, %y(i32)` IR at -O0 + // (-O2 hides it via constant folding). Lower in `ty` instead, + // casting the operand up when needed. + std::string operand = v; + if (t != ty) { + std::string casted = temp(s); + emit(s, {mir::Op::Cast, casted, ty, {v}, t.str(), {}, false, e.range}); + operand = casted; + } + if (ty.is_float()) { + emit(s, {mir::Op::ConstFloat, zero, ty, {}, "0.0", {}, false, e.range}); + } else { + emit(s, {mir::Op::ConstInt, zero, ty, {}, "0", {}, false, e.range}); + } + emit(s, {mir::Op::Binary, r, ty, {zero, operand}, "-", {}, false, e.range}); + } else { + std::string operand = v; + if (t != ty) { + std::string casted = temp(s); + emit(s, {mir::Op::Cast, casted, ty, {v}, t.str(), {}, false, e.range}); + operand = casted; + } + emit(s, {mir::Op::Binary, r, ty, {operand}, u.op, {}, false, e.range}); + } + return {r, ty}; + } + case ast::ExprKind::Binary: { + auto& b = static_cast(e); + auto [l, lt] = lower_expr(*b.lhs, s); + auto [r, rt] = lower_expr(*b.rhs, s); + std::string out = temp(s); + bool cmp = b.op == "==" || b.op == "!=" || b.op == "<" || b.op == "<=" || b.op == ">" || + b.op == ">="; + if (cmp) { + // Comparison operands must share one LLVM integer type. + l = coerce_value(s, l, lt, lt, b.lhs->range); + r = coerce_value(s, r, rt, lt, b.rhs->range); + } else { + l = coerce_value(s, l, lt, ty, b.lhs->range); + r = coerce_value(s, r, rt, ty, b.rhs->range); + } + emit(s, + {cmp ? mir::Op::Compare : mir::Op::Binary, + out, + cmp ? lt : ty, + {l, r}, + b.op, + {}, + false, + e.range}); + return {out, ty}; + } + case ast::ExprKind::Call: { + auto& c = static_cast(e); + if (c.callee->kind == ast::ExprKind::Name) { + const auto& name = static_cast(*c.callee).name; + if (builtin_function(name)) + return lower_builtin_call(c, s); + } + std::string callee; + std::vector args; + if (c.callee->kind == ast::ExprKind::Name) { + auto& n = static_cast(*c.callee); + auto fi = model_.functions.find(n.name); + callee = n.name; + if (fi != model_.functions.end()) + callee = specialize_call(c, fi->second, s); + for (std::size_t i = 0; i < c.args.size(); ++i) { + auto [v, actual] = lower_expr(*c.args[i], s); + if (fi != model_.functions.end() && i < fi->second.params.size()) { + Type expected = subst_type(fi->second.params[i], s); + // For generic callees the formal may still be generic; + // coerce_value is a no-op unless it is a concrete + // int/bool/float mismatch (e.g. i32 literal -> i64 param). + v = coerce_value(s, v, actual, expected, c.args[i]->range); + } + args.push_back(v); + } + } else if (c.callee->kind == ast::ExprKind::Member) { + auto& member = static_cast(*c.callee); + Type receiver_ty = expr_type(*member.base, s); + Type owner = receiver_ty; + if (owner.kind == TypeKind::Ref && owner.pointee) + owner = *owner.pointee; + const auto fi = model_.functions.find(owner.name + "::" + member.member); + if (fi == model_.functions.end()) { + diags_.error( + e.range, "E0507", "internal: method signature missing during MIR lowering"); + return {"undef", ty}; + } + callee = fi->second.name; + if (!fi->second.params.empty() && fi->second.params.front().kind == TypeKind::Ref) { + if (receiver_ty.kind == TypeKind::Ref) + args.push_back(lower_expr(*member.base, s).first); + else + args.push_back(lower_place_address(*member.base, s)); + } else + args.push_back(lower_expr(*member.base, s).first); + // Method params[0] is the receiver already handled above. + const std::size_t param_offset = + fi->second.params.empty() ? 0 : args.size() - c.args.size(); + for (std::size_t i = 0; i < c.args.size(); ++i) { + auto [v, actual] = lower_expr(*c.args[i], s); + if (param_offset + i < fi->second.params.size()) { + Type expected = subst_type(fi->second.params[param_offset + i], s); + v = coerce_value(s, v, actual, expected, c.args[i]->range); + } + args.push_back(v); + } + } else { + diags_.error( + e.range, "E0507", "internal: unsupported call target during MIR lowering"); + return {"undef", ty}; + } + std::string r = ty.is_unit() ? "" : temp(s); + emit(s, {mir::Op::Call, r, ty, std::move(args), callee, {}, false, e.range}); + return {r, ty}; + } + case ast::ExprKind::Cast: { + auto& c = static_cast(e); + auto [v, from] = lower_expr(*c.value, s); + std::string r = temp(s); + emit(s, {mir::Op::Cast, r, ty, {v}, from.str(), {}, false, e.range}); + return {r, ty}; + } + case ast::ExprKind::Asm: { + auto& a = static_cast(e); + mir::Instruction in; + in.op = mir::Op::InlineAsm; + in.type = Type::builtin("unit"); + in.text = a.assembly; + in.flag = a.is_volatile; + in.range = e.range; + for (const auto& o : a.operands) { + mir::AsmOperand mo; + mo.kind = o.kind; + mo.constraint = o.constraint; + if (o.value) { + auto [v, t] = lower_expr(*o.value, s); + mo.value = v; + mo.type = t; + } + if (!o.out_name.empty()) { + auto it = s.slots.find(o.out_name); + if (it != s.slots.end()) { + mo.out_slot = it->second; + mo.type = s.local_types[o.out_name]; + } + } + in.asm_operands.push_back(std::move(mo)); + } + emit(s, std::move(in)); + return {"", ty}; + } + case ast::ExprKind::Await: { + auto& a = static_cast(e); + auto [v, future_ty] = lower_expr(*a.value, s); + (void)future_ty; + std::string r = temp(s); + emit(s, {mir::Op::Await, r, ty, {v}, "", {}, false, e.range}); + return {r, ty}; + } + case ast::ExprKind::Member: { + std::string addr = lower_place_address(e, s); + std::string r = temp(s); + const bool atomic = place_is_atomic(e, s); + emit(s, + {mir::Op::Load, + r, + ty, + {addr}, + atomic ? load_order(e, s) : "", + {}, + atomic, + e.range}); + return {r, ty}; + } + case ast::ExprKind::Index: { + std::string addr = lower_place_address(e, s); + std::string r = temp(s); + emit(s, {mir::Op::Load, r, ty, {addr}, "", {}, false, e.range}); + return {r, ty}; + } + } + return {"undef", ty}; +} + +std::pair MIRLowerer::lower_builtin_call(const ast::CallExpr& call, FnState& s) { + const auto& name = static_cast(*call.callee).name; + const Type result = expr_type(call, s); + auto declare_call = [&](const std::string& runtime_name, + const Type& runtime_result, + const std::vector& parameter_types) { + std::vector parameters; + for (std::size_t i = 0; i < parameter_types.size(); ++i) + parameters.push_back({"p" + std::to_string(i), parameter_types[i]}); + declare_runtime_function(runtime_name, runtime_result, std::move(parameters)); + }; + auto runtime_call = [&](const std::string& runtime_name, + const Type& runtime_result, + const std::vector& parameter_types, + std::vector arguments, + const SourceRange& range) { + declare_call(runtime_name, runtime_result, parameter_types); + const std::string output = runtime_result.is_unit() ? "" : temp(s); + emit(s, + {mir::Op::Call, + output, + runtime_result, + std::move(arguments), + runtime_name, + {}, + false, + range}); + return output; + }; + auto cast_value = + [&](const std::string& value, const Type& from, const Type& to, const SourceRange& range) { + if (from == to) + return value; + const std::string output = temp(s); + emit(s, {mir::Op::Cast, output, to, {value}, from.str(), {}, false, range}); + return output; + }; + + if (name == "print" || name == "println" || name == "eprint" || name == "eprintln") { + const bool stderr_output = name == "eprint" || name == "eprintln"; + const Type i32 = Type::builtin("i32"); + const Type fd_type = i32; + const std::string fd = temp(s); + emit( + s, + {mir::Op::ConstInt, fd, fd_type, {}, stderr_output ? "2" : "1", {}, false, call.range}); + + for (std::size_t i = 0; i < call.args.size(); ++i) { + if (i != 0) { + runtime_call( + "uinx_print_space", Type::builtin("unit"), {fd_type}, {fd}, call.range); + } + auto [value, type] = lower_expr(*call.args[i], s); + if (type.is_integer() && type.kind != TypeKind::I128 && type.kind != TypeKind::U128) { + const Type target = Type::builtin(unsigned_integer_kind(type.kind) ? "u64" : "i64"); + value = cast_value(value, type, target, call.args[i]->range); + runtime_call(unsigned_integer_kind(type.kind) ? "uinx_print_u64" : "uinx_print_i64", + Type::builtin("unit"), + {target, fd_type}, + {value, fd}, + call.args[i]->range); + } else if (type.is_float()) { + value = cast_value(value, type, Type::builtin("f64"), call.args[i]->range); + runtime_call("uinx_print_f64", + Type::builtin("unit"), + {Type::builtin("f64"), fd_type}, + {value, fd}, + call.args[i]->range); + } else if (type.kind == TypeKind::Bool) { + value = cast_value(value, type, Type::builtin("i64"), call.args[i]->range); + runtime_call("uinx_print_bool", + Type::builtin("unit"), + {Type::builtin("i64"), fd_type}, + {value, fd}, + call.args[i]->range); + } else if (type.kind == TypeKind::Char) { + runtime_call("uinx_print_char", + Type::builtin("unit"), + {Type::builtin("i32"), fd_type}, + {value, fd}, + call.args[i]->range); + } else { + runtime_call("uinx_print_cstr", + Type::builtin("unit"), + {Type::ref(Type::builtin("str"), false), fd_type}, + {value, fd}, + call.args[i]->range); + } + } + runtime_call("uinx_print_newline", Type::builtin("unit"), {fd_type}, {fd}, call.range); + return {"", Type::builtin("unit")}; + } + + if (name == "input") { + const Type str_ref = Type::ref(Type::builtin("str"), false); + std::string prompt = "null"; + if (!call.args.empty()) + prompt = lower_expr(*call.args[0], s).first; + const std::string fd = temp(s); + emit(s, {mir::Op::ConstInt, fd, Type::builtin("i32"), {}, "1", {}, false, call.range}); + return {runtime_call("uinx_input_cstr", + str_ref, + {str_ref, Type::builtin("i32")}, + {prompt, fd}, + call.range), + str_ref}; + } + + auto [value, type] = lower_expr(*call.args[0], s); + const Type str_ref = Type::ref(Type::builtin("str"), false); + const auto is_string = [](const Type& checked) { + return checked.kind == TypeKind::Str || (checked.kind == TypeKind::Ref && checked.pointee && + checked.pointee->kind == TypeKind::Str); + }; + if (name == "str") { + if (is_string(type)) + return {value, str_ref}; + if (type.is_integer()) { + const Type target = Type::builtin(unsigned_integer_kind(type.kind) ? "u64" : "i64"); + value = cast_value(value, type, target, call.range); + return {runtime_call(unsigned_integer_kind(type.kind) ? "uinx_string_from_u64" + : "uinx_string_from_i64", + str_ref, + {target}, + {value}, + call.range), + str_ref}; + } + if (type.is_float()) { + value = cast_value(value, type, Type::builtin("f64"), call.range); + return { + runtime_call( + "uinx_string_from_f64", str_ref, {Type::builtin("f64")}, {value}, call.range), + str_ref}; + } + if (type.kind == TypeKind::Bool) { + value = cast_value(value, type, Type::builtin("i64"), call.range); + return { + runtime_call( + "uinx_string_from_bool", str_ref, {Type::builtin("i64")}, {value}, call.range), + str_ref}; + } + if (type.kind == TypeKind::Char) + return { + runtime_call( + "uinx_string_from_char", str_ref, {Type::builtin("i32")}, {value}, call.range), + str_ref}; + } + if (name == "int" || name == "float") { + const Type target = Type::builtin(name == "int" ? "i64" : "f64"); + if (is_string(type)) { + return {runtime_call(name == "int" ? "uinx_parse_i64_cstr" : "uinx_parse_f64_cstr", + target, + {str_ref}, + {value}, + call.range), + target}; + } + return {cast_value(value, type, target, call.range), target}; + } + if (name == "bool") { + if (type.kind == TypeKind::Bool) + return {value, type}; + if (is_string(type)) { + const Type i64 = Type::builtin("i64"); + value = runtime_call("uinx_parse_i64_cstr", i64, {str_ref}, {value}, call.range); + type = i64; + } + const std::string zero = temp(s); + emit(s, {mir::Op::ConstInt, zero, type, {}, "0", {}, false, call.range}); + const std::string output = temp(s); + emit(s, + {mir::Op::Compare, + output, + Type::builtin("bool"), + {value, zero}, + "!=", + {}, + false, + call.range}); + return {output, Type::builtin("bool")}; + } + if (name == "len") { + if (is_string(type)) + return {runtime_call( + "uinx_cstr_len", Type::builtin("usize"), {str_ref}, {value}, call.range), + Type::builtin("usize")}; + } + diags_.error(call.range, "E0507", "internal: unsupported builtin lowering"); + return {"undef", result}; +} +} // namespace uinx diff --git a/src/mir_internal.hpp b/src/mir_internal.hpp new file mode 100644 index 0000000..31d413b --- /dev/null +++ b/src/mir_internal.hpp @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +// Internal helpers shared by the translation units split out of mir.cpp. + +#pragma once + +#include "uinx/mir.hpp" + +#include +#include +#include +#include +#include +#include + +namespace uinx::mir_detail { +inline bool unsigned_integer_kind(TypeKind kind) { + return kind == TypeKind::U8 || kind == TypeKind::U16 || kind == TypeKind::U32 || + kind == TypeKind::U64 || kind == TypeKind::U128 || kind == TypeKind::Usize; +} + +inline bool builtin_function(std::string_view name) { + static const std::unordered_set names{ + "print", "println", "eprint", "eprintln", "input", "str", "int", "float", "bool", "len"}; + return names.contains(name); +} +} // namespace uinx::mir_detail diff --git a/src/mir_optimizer.cpp b/src/mir_optimizer.cpp new file mode 100644 index 0000000..f3f4d9b --- /dev/null +++ b/src/mir_optimizer.cpp @@ -0,0 +1,298 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "mir_internal.hpp" +#include "uinx/mir.hpp" + +#include +#include +#include +#include +#include +#include + +namespace uinx { +using namespace mir_detail; + +void MIROptimizer::remove_unreachable(mir::Function& f) const { + std::unordered_set reachable; + if (f.blocks.empty()) + return; + std::unordered_map labels; + for (std::size_t i = 0; i < f.blocks.size(); ++i) + labels[f.blocks[i].label] = i; + std::vector work{0}; + while (!work.empty()) { + const std::size_t index = work.back(); + work.pop_back(); + if (index >= f.blocks.size() || !reachable.insert(f.blocks[index].label).second) + continue; + const auto& block = f.blocks[index]; + if (block.instructions.empty()) { + if (index + 1 < f.blocks.size()) + work.push_back(index + 1); + continue; + } + const auto& tail = block.instructions.back(); + if (tail.op == mir::Op::Jump && !tail.args.empty()) { + if (auto it = labels.find(tail.args[0]); it != labels.end()) + work.push_back(it->second); + } else if (tail.op == mir::Op::CondJump && tail.args.size() >= 3) { + if (auto it = labels.find(tail.args[1]); it != labels.end()) + work.push_back(it->second); + if (auto it = labels.find(tail.args[2]); it != labels.end()) + work.push_back(it->second); + } else if (tail.op != mir::Op::Return && index + 1 < f.blocks.size()) { + work.push_back(index + 1); + } + } + f.blocks.erase(std::remove_if(f.blocks.begin(), + f.blocks.end(), + [&](const mir::BasicBlock& block) { + return !reachable.contains(block.label); + }), + f.blocks.end()); +} + +void MIROptimizer::forward_local_loads(mir::Function& fn) const { + for (auto& block : fn.blocks) { + std::unordered_map slots; + std::unordered_map aliases; + auto resolve = [&](std::string value) { + for (int depth = 0; depth < 32; ++depth) { + auto it = aliases.find(value); + if (it == aliases.end() || it->second == value) + break; + value = it->second; + } + return value; + }; + std::vector output; + output.reserve(block.instructions.size()); + for (auto instruction : block.instructions) { + for (auto& arg : instruction.args) + arg = resolve(arg); + for (auto& operand : instruction.asm_operands) { + if (!operand.value.empty()) + operand.value = resolve(operand.value); + } + const auto is_local_slot = [](const std::string& address) { + return !address.empty() && address.front() == '%' && + address.find(".slot.") != std::string::npos; + }; + if (instruction.op == mir::Op::Load && !instruction.flag && !instruction.args.empty()) { + const auto it = slots.find(instruction.args[0]); + if (it != slots.end()) { + aliases[instruction.result] = resolve(it->second); + continue; + } + } + if (instruction.op == mir::Op::Store && !instruction.flag && + instruction.args.size() >= 2) { + const std::string& address = instruction.args[1]; + if (is_local_slot(address)) + slots[address] = instruction.args[0]; + else + slots.clear(); + } else if (instruction.op == mir::Op::AddressOf) { + if (!instruction.args.empty()) + slots.erase(instruction.args[0]); + } else if (instruction.op == mir::Op::Call || instruction.op == mir::Op::InlineAsm || + instruction.op == mir::Op::AtomicRmw || instruction.op == mir::Op::Fence || + instruction.op == mir::Op::CompilerFence || + (instruction.op == mir::Op::Load && instruction.flag) || + (instruction.op == mir::Op::Store && instruction.flag)) { + slots.clear(); + } + output.push_back(std::move(instruction)); + } + block.instructions = std::move(output); + } +} + +void MIROptimizer::constant_fold(mir::Function& fn) const { + for (auto& block : fn.blocks) { + std::unordered_map constants; + auto get = [&](const std::string& value, std::int64_t& out) { + if (auto it = constants.find(value); it != constants.end()) { + out = it->second; + return true; + } + char* end = nullptr; + const long long parsed = std::strtoll(value.c_str(), &end, 0); + if (end && *end == '\0' && end != value.c_str()) { + out = static_cast(parsed); + return true; + } + return false; + }; + for (auto& instruction : block.instructions) { + if ((instruction.op == mir::Op::ConstInt || instruction.op == mir::Op::ConstBool || + instruction.op == mir::Op::ConstChar) && + !instruction.result.empty()) { + std::int64_t value{}; + if (get(instruction.text, value)) + constants[instruction.result] = value; + continue; + } + if (instruction.type.kind == TypeKind::I128 || instruction.type.kind == TypeKind::U128) + continue; + if (instruction.op == mir::Op::Binary && !instruction.result.empty()) { + std::int64_t a{}, b{}; + if (instruction.text == "!" && instruction.args.size() == 1 && + get(instruction.args[0], a)) { + const std::int64_t result = instruction.type.kind == TypeKind::Bool ? !a : ~a; + instruction.op = instruction.type.kind == TypeKind::Bool ? mir::Op::ConstBool + : mir::Op::ConstInt; + instruction.args.clear(); + instruction.text = std::to_string(result); + constants[instruction.result] = result; + continue; + } + if (instruction.args.size() != 2 || !get(instruction.args[0], a) || + !get(instruction.args[1], b)) + continue; + std::int64_t result{}; + bool ok = true; + if (instruction.text == "+") + result = static_cast(static_cast(a) + + static_cast(b)); + else if (instruction.text == "-") + result = static_cast(static_cast(a) - + static_cast(b)); + else if (instruction.text == "*") + result = static_cast(static_cast(a) * + static_cast(b)); + else if (instruction.text == "/") { + if (b == 0) + ok = false; + else + result = unsigned_integer_kind(instruction.type.kind) + ? static_cast(static_cast(a) / + static_cast(b)) + : a / b; + } else if (instruction.text == "%") { + if (b == 0) + ok = false; + else + result = unsigned_integer_kind(instruction.type.kind) + ? static_cast(static_cast(a) % + static_cast(b)) + : a % b; + } else if (instruction.text == "&" || instruction.text == "&&") + result = a & b; + else if (instruction.text == "|" || instruction.text == "||") + result = a | b; + else if (instruction.text == "^") + result = a ^ b; + else if (instruction.text == "<<") { + if (b < 0 || b >= 64) + ok = false; + else + result = static_cast(static_cast(a) << b); + } else if (instruction.text == ">>") { + if (b < 0 || b >= 64) + ok = false; + else + result = unsigned_integer_kind(instruction.type.kind) + ? static_cast(static_cast(a) >> b) + : (a >> b); + } else + ok = false; + if (ok) { + instruction.op = mir::Op::ConstInt; + instruction.args.clear(); + instruction.text = std::to_string(result); + constants[instruction.result] = result; + } + } else if (instruction.op == mir::Op::Compare && instruction.args.size() == 2 && + !instruction.result.empty()) { + std::int64_t a{}, b{}; + if (!get(instruction.args[0], a) || !get(instruction.args[1], b)) + continue; + const bool u = unsigned_integer_kind(instruction.type.kind); + bool result = false; + if (instruction.text == "==") + result = a == b; + else if (instruction.text == "!=") + result = a != b; + else if (instruction.text == "<") + result = + u ? static_cast(a) < static_cast(b) : a < b; + else if (instruction.text == "<=") + result = + u ? static_cast(a) <= static_cast(b) : a <= b; + else if (instruction.text == ">") + result = + u ? static_cast(a) > static_cast(b) : a > b; + else if (instruction.text == ">=") + result = + u ? static_cast(a) >= static_cast(b) : a >= b; + instruction.op = mir::Op::ConstBool; + instruction.type = Type::builtin("bool"); + instruction.args.clear(); + instruction.text = result ? "1" : "0"; + constants[instruction.result] = result ? 1 : 0; + } + } + } +} + +void MIROptimizer::eliminate_dead_values(mir::Function& fn) const { + bool changed = true; + while (changed) { + changed = false; + std::unordered_map uses; + for (const auto& block : fn.blocks) + for (const auto& instruction : block.instructions) { + for (const auto& arg : instruction.args) + ++uses[arg]; + for (const auto& operand : instruction.asm_operands) + if (!operand.value.empty()) + ++uses[operand.value]; + } + for (auto& block : fn.blocks) { + auto it = std::remove_if( + block.instructions.begin(), + block.instructions.end(), + [&](const mir::Instruction& instruction) { + if (instruction.result.empty() || uses[instruction.result] != 0) + return false; + bool pure = + instruction.op == mir::Op::Alloca || instruction.op == mir::Op::ConstInt || + instruction.op == mir::Op::ConstFloat || + instruction.op == mir::Op::ConstBool || + instruction.op == mir::Op::ConstChar || + instruction.op == mir::Op::ConstString || + instruction.op == mir::Op::StructInit || + instruction.op == mir::Op::FieldAddr || instruction.op == mir::Op::Binary || + instruction.op == mir::Op::Compare || instruction.op == mir::Op::Cast || + instruction.op == mir::Op::AddressOf || + (instruction.op == mir::Op::Load && !instruction.flag); + if (pure) + changed = true; + return pure; + }); + block.instructions.erase(it, block.instructions.end()); + } + } +} + +void MIROptimizer::run(mir::Module& m) const { + if (level_ <= 0) + return; + for (auto& f : m.functions) { + if (f.is_extern) + continue; + remove_unreachable(f); + forward_local_loads(f); + constant_fold(f); + eliminate_dead_values(f); + if (level_ >= 2) { + forward_local_loads(f); + eliminate_dead_values(f); + } + } +} +} // namespace uinx diff --git a/src/mir_stmt.cpp b/src/mir_stmt.cpp new file mode 100644 index 0000000..09f8a8e --- /dev/null +++ b/src/mir_stmt.cpp @@ -0,0 +1,321 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "mir_internal.hpp" +#include "uinx/mir.hpp" + +#include +#include +#include +#include +#include +#include + +namespace uinx { +using namespace mir_detail; + +bool MIRLowerer::place_is_atomic(const ast::Expr& e, const FnState& s) const { + const Type ty = expr_type(e, s); + // Bool excluded: LLVM i1 atomics are invalid (byte-sized required). + // Use AtomicBool (u8-backed) instead. + const bool compatible = ty.is_integer() || ty.kind == TypeKind::RawPtr; + if (!compatible) + return false; + (void)s; + // Explicit-shared only: smp auto no longer promotes. Only `shared` + // globals/fields lower to LLVM atomics. percpu is never atomic. + if (e.kind == ast::ExprKind::Name) { + const auto& n = static_cast(e); + auto g = model_.globals.find(n.name); + if (g == model_.globals.end() || g->second.is_percpu) + return false; + return g->second.is_shared; + } + if (e.kind == ast::ExprKind::Member) { + const auto& m = static_cast(e); + Type base = expr_type(*m.base, s); + if (base.kind == TypeKind::Ref && base.pointee) + base = *base.pointee; + auto it = model_.structs.find(base.name); + return it != model_.structs.end() && it->second.shared_fields.contains(m.member); + } + return false; +} +std::string MIRLowerer::load_order(const ast::Expr& e, const FnState& s) const { + (void)e; + return s.smp_mode == ast::SmpMode::Strict ? "seq_cst" : "acquire"; +} +std::string MIRLowerer::store_order(const ast::Expr& e, const FnState& s) const { + (void)e; + return s.smp_mode == ast::SmpMode::Strict ? "seq_cst" : "release"; +} +std::string MIRLowerer::rmw_order(const ast::Expr& e, const FnState& s) const { + (void)e; + return s.smp_mode == ast::SmpMode::Strict ? "seq_cst" : "acq_rel"; +} + +void MIRLowerer::lower_stmt(const ast::Stmt& st, FnState& s) { + if (auto l = dynamic_cast(&st)) { + auto [v, init_ty] = lower_expr(*l->init, s); + Type t = init_ty; + if (auto it = model_.binding_types.find(l); it != model_.binding_types.end()) + t = subst_type(it->second, s); + v = coerce_value(s, v, init_ty, t, l->range); + std::string sl = slot(s, l->name); + s.slots[l->name] = sl; + s.local_types[l->name] = t; + s.scope_locals.back().push_back(l->name); + emit(s, {mir::Op::Alloca, sl, t, {}, "", {}, false, l->range}); + emit(s, {mir::Op::Store, "", t, {v, sl}, "", {}, false, l->range}); + return; + } + if (auto a = dynamic_cast(&st)) { + if (auto binding = model_.hir.assignment_bindings.find(a); + binding != model_.hir.assignment_bindings.end()) { + auto [v, init_ty] = lower_expr(*a->value, s); + Type t = init_ty; + if (auto it = model_.assignment_binding_types.find(a); + it != model_.assignment_binding_types.end()) + t = subst_type(it->second, s); + v = coerce_value(s, v, init_ty, t, a->range); + const auto& name = static_cast(*a->target).name; + std::string sl = slot(s, name); + s.slots[name] = sl; + s.local_types[name] = t; + s.scope_locals.back().push_back(name); + emit(s, {mir::Op::Alloca, sl, t, {}, "", {}, false, a->range}); + emit(s, {mir::Op::Store, "", t, {v, sl}, "", {}, false, a->range}); + return; + } + std::string addr = lower_place_address(*a->target, s); + Type target_ty = expr_type(*a->target, s); + auto [v, vt] = lower_expr(*a->value, s); + v = coerce_value(s, v, vt, target_ty, a->range); + const bool atomic = place_is_atomic(*a->target, s); + if (atomic && a->op != "=") { + const std::string op = a->op.substr(0, a->op.size() - 1); + if (op == "+" || op == "-" || op == "&" || op == "|" || op == "^") { + emit(s, + {mir::Op::AtomicRmw, + "", + target_ty, + {addr, v, rmw_order(*a->target, s)}, + op, + {}, + true, + a->range}); + return; + } + diags_.error(a->range, + "E0509", + "atomic compound assignment '" + a->op + + "' is not supported; use an explicit compare/exchange loop"); + return; + } + if (a->op != "=") { + std::string old = temp(s); + emit(s, {mir::Op::Load, old, target_ty, {addr}, "", {}, false, a->range}); + std::string combined = temp(s); + std::string op = a->op.substr(0, a->op.size() - 1); + emit(s, {mir::Op::Binary, combined, target_ty, {old, v}, op, {}, false, a->range}); + v = combined; + } + emit(s, + {mir::Op::Store, + "", + target_ty, + {v, addr}, + atomic ? store_order(*a->target, s) : "", + {}, + atomic, + a->range}); + return; + } + if (auto e = dynamic_cast(&st)) { + (void)lower_expr(*e->expr, s); + return; + } + if (auto r = dynamic_cast(&st)) { + emit_scope_drops(s); + if (r->value) { + auto [v, t] = lower_expr(*r->value, s); + v = coerce_value(s, v, t, s.fn.result, r->range); + emit(s, {mir::Op::Return, "", s.fn.result, {v}, "", {}, false, r->range}); + } else + emit(s, {mir::Op::Return, "", Type::builtin("unit"), {}, "", {}, false, r->range}); + return; + } + if (auto f = dynamic_cast(&st)) { + emit(s, + {f->compiler_only ? mir::Op::CompilerFence : mir::Op::Fence, + "", + Type::builtin("unit"), + {}, + f->order, + {}, + false, + f->range}); + return; + } + if (auto b = dynamic_cast(&st)) { + lower_block(*b, s); + return; + } + if (auto u = dynamic_cast(&st)) { + lower_block(*u->body, s); + return; + } + if (auto i = dynamic_cast(&st)) { + auto [c, ct] = lower_expr(*i->condition, s); + (void)ct; + std::size_t then_i = new_block(s, "if.then." + std::to_string(s.fn.blocks.size())); + std::size_t else_i = new_block(s, "if.else." + std::to_string(s.fn.blocks.size())); + std::size_t merge_i = new_block(s, "if.end." + std::to_string(s.fn.blocks.size())); + std::string then_l = s.fn.blocks[then_i].label, else_l = s.fn.blocks[else_i].label, + merge_l = s.fn.blocks[merge_i].label; + emit(s, + {mir::Op::CondJump, + "", + Type::builtin("bool"), + {c, then_l, else_l}, + "", + {}, + false, + i->range}); + s.block = then_i; + lower_block(*i->then_block, s); + if (!block(s).terminated) + emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {merge_l}, "", {}, false, i->range}); + s.block = else_i; + if (i->else_block) + lower_block(*i->else_block, s); + if (!block(s).terminated) + emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {merge_l}, "", {}, false, i->range}); + s.block = merge_i; + return; + } + if (auto w = dynamic_cast(&st)) { + std::size_t cond_i = new_block(s, "while.cond." + std::to_string(s.fn.blocks.size())); + std::size_t body_i = new_block(s, "while.body." + std::to_string(s.fn.blocks.size())); + std::size_t end_i = new_block(s, "while.end." + std::to_string(s.fn.blocks.size())); + std::string cl = s.fn.blocks[cond_i].label, bl = s.fn.blocks[body_i].label, + el = s.fn.blocks[end_i].label; + emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {cl}, "", {}, false, w->range}); + s.block = cond_i; + auto [c, t] = lower_expr(*w->condition, s); + (void)t; + emit(s, + {mir::Op::CondJump, "", Type::builtin("bool"), {c, bl, el}, "", {}, false, w->range}); + s.block = body_i; + s.loop_targets.push_back({cond_i, end_i}); + lower_block(*w->body, s); + s.loop_targets.pop_back(); + if (!block(s).terminated) + emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {cl}, "", {}, false, w->range}); + s.block = end_i; + return; + } + if (auto f = dynamic_cast(&st)) { + auto [begin_value, iter_ty] = lower_expr(*f->begin, s); + auto [end_value, end_ty] = lower_expr(*f->end, s); + (void)end_ty; + std::string iter_slot = slot(s, f->name); + s.slots[f->name] = iter_slot; + s.local_types[f->name] = iter_ty; + s.scope_locals.back().push_back(f->name); + emit(s, {mir::Op::Alloca, iter_slot, iter_ty, {}, "", {}, false, f->range}); + emit(s, {mir::Op::Store, "", iter_ty, {begin_value, iter_slot}, "", {}, false, f->range}); + std::size_t cond_i = new_block(s, "for.cond." + std::to_string(s.fn.blocks.size())); + std::size_t body_i = new_block(s, "for.body." + std::to_string(s.fn.blocks.size())); + std::size_t step_i = new_block(s, "for.step." + std::to_string(s.fn.blocks.size())); + std::size_t end_i = new_block(s, "for.end." + std::to_string(s.fn.blocks.size())); + const std::string cl = s.fn.blocks[cond_i].label, bl = s.fn.blocks[body_i].label, + sl = s.fn.blocks[step_i].label, el = s.fn.blocks[end_i].label; + emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {cl}, "", {}, false, f->range}); + s.block = cond_i; + std::string current = temp(s); + emit(s, {mir::Op::Load, current, iter_ty, {iter_slot}, "", {}, false, f->range}); + std::string cond = temp(s); + emit(s, + {mir::Op::Compare, + cond, + iter_ty, + {current, end_value}, + f->inclusive ? "<=" : "<", + {}, + false, + f->range}); + emit(s, + {mir::Op::CondJump, + "", + Type::builtin("bool"), + {cond, bl, el}, + "", + {}, + false, + f->range}); + s.block = body_i; + s.loop_targets.push_back({step_i, end_i}); + lower_block(*f->body, s); + s.loop_targets.pop_back(); + if (!block(s).terminated) + emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {sl}, "", {}, false, f->range}); + s.block = step_i; + std::string old = temp(s); + emit(s, {mir::Op::Load, old, iter_ty, {iter_slot}, "", {}, false, f->range}); + std::string one = temp(s); + emit(s, {mir::Op::ConstInt, one, iter_ty, {}, "1", {}, false, f->range}); + std::string next = temp(s); + emit(s, {mir::Op::Binary, next, iter_ty, {old, one}, "+", {}, false, f->range}); + emit(s, {mir::Op::Store, "", iter_ty, {next, iter_slot}, "", {}, false, f->range}); + emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {cl}, "", {}, false, f->range}); + s.block = end_i; + return; + } + if (auto l = dynamic_cast(&st)) { + std::size_t body_i = new_block(s, "loop.body." + std::to_string(s.fn.blocks.size())); + std::size_t end_i = new_block(s, "loop.end." + std::to_string(s.fn.blocks.size())); + const std::string bl = s.fn.blocks[body_i].label, el = s.fn.blocks[end_i].label; + emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {bl}, "", {}, false, l->range}); + s.block = body_i; + s.loop_targets.push_back({body_i, end_i}); + lower_block(*l->body, s); + s.loop_targets.pop_back(); + if (!block(s).terminated) + emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {bl}, "", {}, false, l->range}); + s.block = end_i; + return; + } + if (dynamic_cast(&st)) { + if (!s.loop_targets.empty()) { + emit_scope_drops(s); + emit(s, + {mir::Op::Jump, + "", + Type::builtin("unit"), + {s.fn.blocks[s.loop_targets.back().second].label}, + "", + {}, + false, + st.range}); + } + return; + } + if (dynamic_cast(&st)) { + if (!s.loop_targets.empty()) { + emit_scope_drops(s); + emit(s, + {mir::Op::Jump, + "", + Type::builtin("unit"), + {s.fn.blocks[s.loop_targets.back().first].label}, + "", + {}, + false, + st.range}); + } + return; + } +} +} // namespace uinx diff --git a/src/sema_internal.hpp b/src/sema_internal.hpp index 21920c6..2ff1c9d 100644 --- a/src/sema_internal.hpp +++ b/src/sema_internal.hpp @@ -69,4 +69,97 @@ inline bool builtin_function(std::string_view name) { "print", "println", "eprint", "eprintln", "input", "str", "int", "float", "bool", "len"}; return names.contains(name); } + +// Calls `f` on every direct child expression of `expr`. A method call visits the +// receiver (`callee` base) and the arguments; a plain call only its arguments. +inline void for_each_child_expr(const ast::Expr& expr, + const std::function& f) { + switch (expr.kind) { + case ast::ExprKind::Unary: + f(*static_cast(expr).operand); + break; + case ast::ExprKind::Binary: { + const auto& binary = static_cast(expr); + f(*binary.lhs); + f(*binary.rhs); + break; + } + case ast::ExprKind::Borrow: + f(*static_cast(expr).target); + break; + case ast::ExprKind::Call: { + const auto& call = static_cast(expr); + if (call.callee->kind == ast::ExprKind::Member) + f(*static_cast(*call.callee).base); + for (const auto& arg : call.args) + f(*arg); + break; + } + case ast::ExprKind::Member: + f(*static_cast(expr).base); + break; + case ast::ExprKind::Index: { + const auto& index = static_cast(expr); + f(*index.base); + f(*index.index); + break; + } + case ast::ExprKind::Await: + f(*static_cast(expr).value); + break; + case ast::ExprKind::Cast: + f(*static_cast(expr).value); + break; + case ast::ExprKind::StructLiteral: + for (const auto& field : static_cast(expr).fields) + f(*field.value); + break; + case ast::ExprKind::Asm: + for (const auto& operand : static_cast(expr).operands) + if (operand.value) + f(*operand.value); + break; + default: + break; + } +} + +// Calls `f` on every statement-level expression reachable from `stmt`, +// descending into nested blocks. +inline void walk_stmt_exprs(const ast::Stmt& stmt, const std::function& f) { + const auto block = [&](const ast::BlockStmt* body) { + if (body) + for (const auto& child : body->stmts) + walk_stmt_exprs(*child, f); + }; + if (const auto* let_stmt = dynamic_cast(&stmt)) { + if (let_stmt->init) + f(*let_stmt->init); + } else if (const auto* assign_stmt = dynamic_cast(&stmt)) { + f(*assign_stmt->target); + f(*assign_stmt->value); + } else if (const auto* expr_stmt = dynamic_cast(&stmt)) { + f(*expr_stmt->expr); + } else if (const auto* return_stmt = dynamic_cast(&stmt)) { + if (return_stmt->value) + f(*return_stmt->value); + } else if (const auto* if_stmt = dynamic_cast(&stmt)) { + f(*if_stmt->condition); + block(if_stmt->then_block.get()); + block(if_stmt->else_block.get()); + } else if (const auto* while_stmt = dynamic_cast(&stmt)) { + f(*while_stmt->condition); + block(while_stmt->body.get()); + } else if (const auto* for_stmt = dynamic_cast(&stmt)) { + f(*for_stmt->begin); + f(*for_stmt->end); + block(for_stmt->body.get()); + } else if (const auto* loop_stmt = dynamic_cast(&stmt)) { + block(loop_stmt->body.get()); + } else if (const auto* block_stmt = dynamic_cast(&stmt)) { + block(block_stmt); + } else if (const auto* unsafe_stmt = dynamic_cast(&stmt)) { + block(unsafe_stmt->body.get()); + } +} } // namespace uinx::sema_detail diff --git a/src/sema_smp.cpp b/src/sema_smp.cpp index bd1002c..aa2ea18 100644 --- a/src/sema_smp.cpp +++ b/src/sema_smp.cpp @@ -47,111 +47,28 @@ void TypeChecker::validate_concurrency_boundaries() { void TypeChecker::propagate_concurrency(ast::SmpMode mode) { std::unordered_map> edges; - std::function&)> visit_expr; - std::function&)> visit_stmt; - visit_expr = [&](const ast::Expr& expr, std::unordered_set& out) { - switch (expr.kind) { - case ast::ExprKind::Unary: - visit_expr(*static_cast(expr).operand, out); - break; - case ast::ExprKind::Binary: { - const auto& b = static_cast(expr); - visit_expr(*b.lhs, out); - visit_expr(*b.rhs, out); - break; - } - case ast::ExprKind::Borrow: - visit_expr(*static_cast(expr).target, out); - break; - case ast::ExprKind::Call: { - const auto& call = static_cast(expr); - if (call.callee->kind == ast::ExprKind::Name) { - const auto& name = static_cast(*call.callee).name; - if (model_.functions.contains(name)) - out.insert(name); - } else if (call.callee->kind == ast::ExprKind::Member) { - const auto& member = static_cast(*call.callee); - auto type_it = model_.expr_types.find(member.base.get()); - if (type_it != model_.expr_types.end()) { - Type owner = type_it->second; - if (owner.kind == TypeKind::Ref && owner.pointee) - owner = *owner.pointee; - const std::string key = owner.name + "::" + member.member; - if (model_.functions.contains(key)) - out.insert(key); - } - visit_expr(*member.base, out); + std::function&)> collect_callees; + collect_callees = [&](const ast::Expr& expr, std::unordered_set& out) { + if (expr.kind == ast::ExprKind::Call) { + const auto& call = static_cast(expr); + if (call.callee->kind == ast::ExprKind::Name) { + const auto& name = static_cast(*call.callee).name; + if (model_.functions.contains(name)) + out.insert(name); + } else if (call.callee->kind == ast::ExprKind::Member) { + const auto& member = static_cast(*call.callee); + auto type_it = model_.expr_types.find(member.base.get()); + if (type_it != model_.expr_types.end()) { + Type owner = type_it->second; + if (owner.kind == TypeKind::Ref && owner.pointee) + owner = *owner.pointee; + const std::string key = owner.name + "::" + member.member; + if (model_.functions.contains(key)) + out.insert(key); } - for (const auto& arg : call.args) - visit_expr(*arg, out); - break; } - case ast::ExprKind::Member: - visit_expr(*static_cast(expr).base, out); - break; - case ast::ExprKind::Index: { - const auto& i = static_cast(expr); - visit_expr(*i.base, out); - visit_expr(*i.index, out); - break; - } - case ast::ExprKind::Await: - visit_expr(*static_cast(expr).value, out); - break; - case ast::ExprKind::Cast: - visit_expr(*static_cast(expr).value, out); - break; - case ast::ExprKind::StructLiteral: - for (const auto& field : static_cast(expr).fields) - visit_expr(*field.value, out); - break; - case ast::ExprKind::Asm: - for (const auto& operand : static_cast(expr).operands) - if (operand.value) - visit_expr(*operand.value, out); - break; - default: - break; - } - }; - visit_stmt = [&](const ast::Stmt& stmt, std::unordered_set& out) { - if (const auto* let_stmt = dynamic_cast(&stmt)) { - if (let_stmt->init) - visit_expr(*let_stmt->init, out); - } else if (const auto* assign_stmt = dynamic_cast(&stmt)) { - visit_expr(*assign_stmt->target, out); - visit_expr(*assign_stmt->value, out); - } else if (const auto* expr_stmt = dynamic_cast(&stmt)) - visit_expr(*expr_stmt->expr, out); - else if (const auto* return_stmt = dynamic_cast(&stmt)) { - if (return_stmt->value) - visit_expr(*return_stmt->value, out); - } else if (const auto* if_stmt = dynamic_cast(&stmt)) { - visit_expr(*if_stmt->condition, out); - for (const auto& child : if_stmt->then_block->stmts) - visit_stmt(*child, out); - if (if_stmt->else_block) - for (const auto& child : if_stmt->else_block->stmts) - visit_stmt(*child, out); - } else if (const auto* while_stmt = dynamic_cast(&stmt)) { - visit_expr(*while_stmt->condition, out); - for (const auto& child : while_stmt->body->stmts) - visit_stmt(*child, out); - } else if (const auto* for_stmt = dynamic_cast(&stmt)) { - visit_expr(*for_stmt->begin, out); - visit_expr(*for_stmt->end, out); - for (const auto& child : for_stmt->body->stmts) - visit_stmt(*child, out); - } else if (const auto* loop_stmt = dynamic_cast(&stmt)) { - for (const auto& child : loop_stmt->body->stmts) - visit_stmt(*child, out); - } else if (const auto* block_stmt = dynamic_cast(&stmt)) { - for (const auto& child : block_stmt->stmts) - visit_stmt(*child, out); - } else if (const auto* unsafe_stmt = dynamic_cast(&stmt)) { - for (const auto& child : unsafe_stmt->body->stmts) - visit_stmt(*child, out); } + for_each_child_expr(expr, [&](const ast::Expr& child) { collect_callees(child, out); }); }; for (const auto& [key, sig] : model_.functions) { @@ -159,7 +76,7 @@ void TypeChecker::propagate_concurrency(ast::SmpMode mode) { continue; auto& callees = edges[key]; for (const auto& stmt : sig.decl->body->stmts) - visit_stmt(*stmt, callees); + walk_stmt_exprs(*stmt, [&](const ast::Expr& expr) { collect_callees(expr, callees); }); } std::vector work; @@ -198,11 +115,22 @@ void TypeChecker::propagate_concurrency(ast::SmpMode mode) { return type.is_integer() || type.kind == TypeKind::RawPtr; }; + // A local or parameter that shadows a global is not a global access. + const auto is_local = [&](const ast::Expr& name_expr) { + const auto resolved = model_.hir.expr_resolution.find(&name_expr); + if (resolved == model_.hir.expr_resolution.end() || + resolved->second >= model_.hir.symbols.size()) + return false; + const auto kind = model_.hir.symbols[resolved->second].kind; + return kind == hir::SymbolKind::Local || kind == hir::SymbolKind::Parameter; + }; + std::unordered_set reported; std::function check_expr; - std::function check_stmt; check_expr = [&](const ast::Expr& expr) { if (expr.kind == ast::ExprKind::Name) { + if (is_local(expr)) + return; const auto& name = static_cast(expr).name; auto global = model_.globals.find(name); if (global != model_.globals.end() && global->second.is_mut && @@ -231,7 +159,7 @@ void TypeChecker::propagate_concurrency(ast::SmpMode mode) { } if (expr.kind == ast::ExprKind::Member) { const auto& member = static_cast(expr); - if (member.base->kind == ast::ExprKind::Name) { + if (member.base->kind == ast::ExprKind::Name && !is_local(*member.base)) { const auto& base_name = static_cast(*member.base).name; auto global = model_.globals.find(base_name); if (global != model_.globals.end() && global->second.is_mut && @@ -276,97 +204,14 @@ void TypeChecker::propagate_concurrency(ast::SmpMode mode) { check_expr(*member.base); return; } - switch (expr.kind) { - case ast::ExprKind::Unary: - check_expr(*static_cast(expr).operand); - break; - case ast::ExprKind::Binary: { - const auto& binary = static_cast(expr); - check_expr(*binary.lhs); - check_expr(*binary.rhs); - break; - } - case ast::ExprKind::Borrow: - check_expr(*static_cast(expr).target); - break; - case ast::ExprKind::Call: { - const auto& call = static_cast(expr); - if (call.callee->kind == ast::ExprKind::Member) - check_expr(*static_cast(*call.callee).base); - for (const auto& arg : call.args) - check_expr(*arg); - break; - } - case ast::ExprKind::Index: { - const auto& index = static_cast(expr); - check_expr(*index.base); - check_expr(*index.index); - break; - } - case ast::ExprKind::Await: - check_expr(*static_cast(expr).value); - break; - case ast::ExprKind::Cast: - check_expr(*static_cast(expr).value); - break; - case ast::ExprKind::StructLiteral: - for (const auto& field : static_cast(expr).fields) - check_expr(*field.value); - break; - case ast::ExprKind::Asm: - for (const auto& operand : static_cast(expr).operands) - if (operand.value) - check_expr(*operand.value); - break; - default: - break; - } - }; - check_stmt = [&](const ast::Stmt& stmt) { - if (const auto* let_stmt = dynamic_cast(&stmt)) { - if (let_stmt->init) - check_expr(*let_stmt->init); - } else if (const auto* assign_stmt = dynamic_cast(&stmt)) { - check_expr(*assign_stmt->target); - check_expr(*assign_stmt->value); - } else if (const auto* expr_stmt = dynamic_cast(&stmt)) - check_expr(*expr_stmt->expr); - else if (const auto* return_stmt = dynamic_cast(&stmt)) { - if (return_stmt->value) - check_expr(*return_stmt->value); - } else if (const auto* if_stmt = dynamic_cast(&stmt)) { - check_expr(*if_stmt->condition); - for (const auto& child : if_stmt->then_block->stmts) - check_stmt(*child); - if (if_stmt->else_block) - for (const auto& child : if_stmt->else_block->stmts) - check_stmt(*child); - } else if (const auto* while_stmt = dynamic_cast(&stmt)) { - check_expr(*while_stmt->condition); - for (const auto& child : while_stmt->body->stmts) - check_stmt(*child); - } else if (const auto* for_stmt = dynamic_cast(&stmt)) { - check_expr(*for_stmt->begin); - check_expr(*for_stmt->end); - for (const auto& child : for_stmt->body->stmts) - check_stmt(*child); - } else if (const auto* loop_stmt = dynamic_cast(&stmt)) { - for (const auto& child : loop_stmt->body->stmts) - check_stmt(*child); - } else if (const auto* block_stmt = dynamic_cast(&stmt)) { - for (const auto& child : block_stmt->stmts) - check_stmt(*child); - } else if (const auto* unsafe_stmt = dynamic_cast(&stmt)) { - for (const auto& child : unsafe_stmt->body->stmts) - check_stmt(*child); - } + for_each_child_expr(expr, check_expr); }; for (const auto& [key, signature] : model_.functions) { if (!signature.is_concurrent || !signature.decl || !signature.decl->body) continue; for (const auto& statement : signature.decl->body->stmts) - check_stmt(*statement); + walk_stmt_exprs(*statement, check_expr); } } } // namespace uinx diff --git a/tests/conformance/pass/smp_local_shadows_global.ux b/tests/conformance/pass/smp_local_shadows_global.ux new file mode 100644 index 0000000..9982c1f --- /dev/null +++ b/tests/conformance/pass/smp_local_shadows_global.ux @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: conformance.positive.smp-local-shadows-global +# EXPECT: compile-pass +# A local or parameter named like a mutable global is not a global access. + +dontneed std +smp auto + +static var counter: u64 = 0 +public unsafe concurrent func f() -> u64: + var counter: u64 = 1 + counter += 1 + return counter + +public unsafe concurrent func g(counter: u64) -> u64: + return counter + 1 diff --git a/tests/safety/smp-fail/concurrent_global_in_cast.ux b/tests/safety/smp-fail/concurrent_global_in_cast.ux new file mode 100644 index 0000000..bea3ecc --- /dev/null +++ b/tests/safety/smp-fail/concurrent_global_in_cast.ux @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: safety.smp.global-in-cast +# EXPECT: compile-fail +# EXPECT-DIAGNOSTIC: E0363 +# Walker coverage: a mutable global under a cast in a let initializer. + +dontneed std +smp auto + +static var n: u64 = 3 +public unsafe concurrent func f() -> u32: + val a = n as u32 + return a diff --git a/tests/safety/smp-fail/concurrent_global_in_condition.ux b/tests/safety/smp-fail/concurrent_global_in_condition.ux new file mode 100644 index 0000000..81e81ca --- /dev/null +++ b/tests/safety/smp-fail/concurrent_global_in_condition.ux @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: safety.smp.global-in-condition +# EXPECT: compile-fail +# EXPECT-DIAGNOSTIC: E0363 +# Walker coverage: a mutable global in a loop condition. + +dontneed std +smp auto + +static var counter: u64 = 0 +public unsafe concurrent func f() -> u64: + while counter < 3: + return 1 + return 0