9 Commits

Author SHA1 Message Date
ykiko
8f714c3b4a refactor(document links): use Lexer for unified directive argument scanning
Replace hand-written character scanning with the project's Lexer class
to find filename arguments in preprocessor directives. Extend the Lexer
to activate header_name mode for #embed and expose set_header_name_mode()
for __has_include/__has_embed contexts. Remove unused Include::filename_range
field which had a latent assert crash on macro-expanded includes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:02:22 +08:00
ykiko
ccc805d0c3 Merge branch 'main' of https://github.com/clice-io/clice into feat/document-links-pch-embed 2026-04-09 19:44:34 +08:00
ykiko
d48236de9c refactor: unify include handling with add_link_by_location
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:12:14 +08:00
ykiko
b691ed1d06 refactor: extract add_link_by_location to deduplicate has_include/embed/has_embed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:08:27 +08:00
ykiko
02e4f74347 style: remove verbose comments from integration tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:06:13 +08:00
ykiko
8af2704723 refactor: reuse find_filename_range for has_include scanning
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:01:00 +08:00
ykiko
4d8c335c0d fix: re-lookup session after co_await to avoid invalidated iterator
The sessions DenseMap iterator may be invalidated during co_await
(other coroutines can modify the map). Re-lookup by path_id after
the await completes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 16:56:08 +08:00
ykiko
4926b4ac32 test(document links): add __has_embed integration tests
Cover both existing-file (produces link) and missing-file (no link)
cases for __has_embed directives.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 16:05:54 +08:00
ykiko
13527b7084 feat(feature): preserve PCH document links and add #embed/#has_embed support
PCH compilation now serializes document links and stores them in PCHState.
The master server merges PCH links with main-file links on DocumentLink
requests, fixing missing links for includes inside the preamble.

Also adds document link support for #embed and __has_embed directives.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:56:19 +08:00
100 changed files with 1222 additions and 9561 deletions

View File

@@ -100,7 +100,7 @@ SortIncludes: true
SortUsingDeclarations: Never
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '^["<](spdlog|toml\+\+|coraing|cpptrace|flatbuffers|kota)/'
- Regex: '^["<](spdlog|toml\+\+|coraing|cpptrace|flatbuffers)/'
Priority: 30
SortPriority: 31

View File

@@ -13,7 +13,7 @@ runs:
- name: Setup Pixi
uses: prefix-dev/setup-pixi@v0.9.3
with:
pixi-version: v0.67.0
pixi-version: v0.62.0
environments: ${{ inputs.environments }}
activate-environment: true
cache: true

View File

@@ -21,7 +21,7 @@ jobs:
- name: Build scan_benchmark
run: |
pixi run cmake-config RelWithDebInfo ON -- -DCLICE_ENABLE_BENCHMARK=ON
pixi run cmake-config RelWithDebInfo ON "-DCLICE_ENABLE_BENCHMARK=ON"
cmake --build build/RelWithDebInfo --target scan_benchmark
- name: Clone LLVM

View File

@@ -1,22 +1,6 @@
name: build llvm
on:
workflow_dispatch:
inputs:
llvm_version:
description: "LLVM version to build (e.g., 21.1.8)"
required: true
type: string
skip_upload:
description: "Skip upload and PR creation (build-only mode)"
required: false
type: boolean
default: false
skip_pr:
description: "Skip PR creation (upload only, no PR)"
required: false
type: boolean
default: false
pull_request:
# if you want to run this workflow, change the branch name to main,
# if you want to turn off it, change it to non existent branch.
@@ -28,7 +12,9 @@ jobs:
fail-fast: false
matrix:
include:
# Native builds
- os: windows-2025
llvm_mode: Debug
lto: OFF
- os: windows-2025
llvm_mode: RelWithDebInfo
lto: OFF
@@ -53,42 +39,6 @@ jobs:
- os: macos-15
llvm_mode: RelWithDebInfo
lto: ON
# Cross-compilation builds
# macOS x64 (from arm64 macos-15)
- os: macos-15
llvm_mode: RelWithDebInfo
lto: OFF
target_triple: x86_64-apple-darwin
- os: macos-15
llvm_mode: RelWithDebInfo
lto: ON
target_triple: x86_64-apple-darwin
# Linux aarch64 (from x64 ubuntu-24.04)
- os: ubuntu-24.04
llvm_mode: RelWithDebInfo
lto: OFF
target_triple: aarch64-linux-gnu
pixi_env: cross-linux-aarch64
- os: ubuntu-24.04
llvm_mode: RelWithDebInfo
lto: ON
target_triple: aarch64-linux-gnu
pixi_env: cross-linux-aarch64
# Windows arm64 (from x64 windows-2025)
- os: windows-2025
llvm_mode: RelWithDebInfo
lto: OFF
target_triple: aarch64-pc-windows-msvc
pixi_env: cross-windows-arm64
- os: windows-2025
llvm_mode: RelWithDebInfo
lto: ON
target_triple: aarch64-pc-windows-msvc
pixi_env: cross-windows-arm64
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
@@ -117,91 +67,49 @@ jobs:
free -h
df -h
- uses: ./.github/actions/setup-pixi
- name: Setup Pixi
uses: prefix-dev/setup-pixi@v0.9.3
with:
environments: ${{ matrix.pixi_env || 'package' }}
pixi-version: v0.59.0
environments: package
activate-environment: true
cache: true
locked: true
- name: Clone llvm-project
- name: Clone llvm-project (21.1.4)
shell: bash
run: |
VERSION="${{ inputs.llvm_version || '21.1.8' }}"
echo "Cloning LLVM ${VERSION}..."
git clone --branch "llvmorg-${VERSION}" --depth 1 https://github.com/llvm/llvm-project.git .llvm
- name: Validate distribution components
shell: bash
run: |
python3 scripts/validate-llvm-components.py \
--llvm-src=.llvm \
--components-file=scripts/llvm-components.json
git clone --branch llvmorg-21.1.4 --depth 1 https://github.com/llvm/llvm-project.git .llvm
- name: Build LLVM (install-distribution)
shell: bash
run: |
ENV="${{ matrix.pixi_env || 'package' }}"
EXTRA_ARGS=""
if [[ -n "${{ matrix.target_triple }}" ]]; then
EXTRA_ARGS="--target-triple=${{ matrix.target_triple }}"
fi
pixi run -e "$ENV" build-llvm \
--llvm-src=.llvm \
--mode="${{ matrix.llvm_mode }}" \
--lto="${{ matrix.lto }}" \
--build-dir=build \
${EXTRA_ARGS}
pixi run build-llvm --llvm-src=.llvm --mode="${{ matrix.llvm_mode }}" --lto="${{ matrix.lto }}" --build-dir=build
- name: Build clice using installed LLVM
if: ${{ !matrix.target_triple }}
shell: bash
run: |
pixi run cmake-config ${{ matrix.llvm_mode }} ON -- \
"-DCLICE_ENABLE_LTO=${{ matrix.lto }}" \
"-DLLVM_INSTALL_PATH=.llvm/build-install"
pixi run cmake-build ${{ matrix.llvm_mode }}
- name: Build clice using installed LLVM (cross-compile)
if: ${{ matrix.target_triple }}
shell: bash
run: |
ENV="${{ matrix.pixi_env || 'package' }}"
pixi run -e "$ENV" cmake-config ${{ matrix.llvm_mode }} ON -- \
"-DCLICE_ENABLE_LTO=${{ matrix.lto }}" \
"-DCLICE_TARGET_TRIPLE=${{ matrix.target_triple }}" \
"-DLLVM_INSTALL_PATH=.llvm/build-install"
pixi run -e "$ENV" cmake-build ${{ matrix.llvm_mode }}
- name: Verify cross-compiled binary architecture
if: ${{ matrix.target_triple && runner.os != 'Windows' }}
shell: bash
run: |
BINARY="build/${{ matrix.llvm_mode }}/bin/clice"
echo "Binary info:"
file "$BINARY"
case "${{ matrix.target_triple }}" in
aarch64-linux-gnu) file "$BINARY" | grep -q "aarch64" ;;
x86_64-apple-darwin) file "$BINARY" | grep -q "x86_64" ;;
esac
- name: Upload cross-compiled clice for functional test
if: ${{ matrix.target_triple && matrix.lto == 'OFF' }}
uses: actions/upload-artifact@v4
with:
name: cross-clice-${{ matrix.target_triple }}-${{ matrix.llvm_mode }}
path: |
build/${{ matrix.llvm_mode }}/bin/
build/${{ matrix.llvm_mode }}/lib/
if-no-files-found: error
retention-days: 1
cmake -B build -G Ninja \
-DCMAKE_BUILD_TYPE=${{ matrix.llvm_mode }} \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchain.cmake \
-DCLICE_ENABLE_TEST=ON \
-DCLICE_CI_ENVIRONMENT=ON \
-DCLICE_ENABLE_LTO=${{ matrix.lto }} \
-DLLVM_INSTALL_PATH=".llvm/build-install"
cmake --build build
- name: Run tests
if: ${{ !matrix.target_triple }}
shell: bash
run: pixi run test ${{ matrix.llvm_mode }}
run: |
EXE_EXT=""
if [[ "${{ runner.os }}" == "Windows" ]]; then
EXE_EXT=".exe"
fi
./build/bin/unit_tests${EXE_EXT} --test-dir="./tests/data"
uv run --project tests pytest -s --log-cli-level=INFO tests/integration --executable=./build/bin/clice${EXE_EXT}
# Prune is only supported for native builds (requires linking clice to test).
# Cross-compiled targets reuse the native prune manifest of the same OS.
- name: Prune LLVM static libraries (Debug/RelWithDebInfo no LTO)
if: (!matrix.target_triple) && (matrix.llvm_mode == 'Debug' || (matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF'))
if: matrix.llvm_mode == 'Debug' || (matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF')
shell: bash
run: |
MANIFEST="pruned-libs-${{ matrix.os }}.json"
@@ -209,13 +117,13 @@ jobs:
python3 scripts/prune-llvm-bin.py \
--action discover \
--install-dir ".llvm/build-install/lib" \
--build-dir "build/${{ matrix.llvm_mode }}" \
--build-dir "build" \
--max-attempts 60 \
--sleep-seconds 60 \
--manifest "${MANIFEST}"
- name: Upload pruned-libs manifest
if: (!matrix.target_triple) && matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF'
if: matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF'
uses: actions/upload-artifact@v4
with:
name: llvm-pruned-libs-${{ matrix.os }}
@@ -223,8 +131,8 @@ jobs:
if-no-files-found: error
compression-level: 0
- name: Apply pruned-libs manifest (RelWithDebInfo + LTO, native only)
if: (!matrix.target_triple) && matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'ON'
- name: Apply pruned-libs manifest (RelWithDebInfo + LTO)
if: matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'ON'
shell: bash
env:
GH_TOKEN: ${{ github.token }}
@@ -234,27 +142,7 @@ jobs:
--action apply \
--manifest "${MANIFEST}" \
--install-dir ".llvm/build-install/lib" \
--build-dir "build/${{ matrix.llvm_mode }}" \
--gh-run-id "${{ github.run_id }}" \
--gh-artifact "llvm-pruned-libs-${{ matrix.os }}" \
--gh-download-dir "artifacts" \
--max-attempts 60 \
--sleep-seconds 60
# For cross-compiled LTO builds, apply the native prune manifest.
# The unused library set is arch-independent (same API surface).
- name: Apply pruned-libs manifest (cross-compile + LTO)
if: matrix.target_triple && matrix.lto == 'ON'
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
MANIFEST="pruned-libs-${{ matrix.os }}.json"
python3 scripts/prune-llvm-bin.py \
--action apply \
--manifest "${MANIFEST}" \
--install-dir ".llvm/build-install/lib" \
--build-dir "build/${{ matrix.llvm_mode }}" \
--build-dir "build" \
--gh-run-id "${{ github.run_id }}" \
--gh-artifact "llvm-pruned-libs-${{ matrix.os }}" \
--gh-download-dir "artifacts" \
@@ -269,35 +157,23 @@ jobs:
MODE_TAG="debug"
fi
# Determine arch/platform/toolchain from target triple or runner OS
if [[ -n "${{ matrix.target_triple }}" ]]; then
case "${{ matrix.target_triple }}" in
x86_64-apple-darwin)
ARCH="x64"; PLATFORM="macos"; TOOLCHAIN="clang" ;;
aarch64-linux-gnu)
ARCH="aarch64"; PLATFORM="linux"; TOOLCHAIN="gnu" ;;
aarch64-pc-windows-msvc)
ARCH="aarch64"; PLATFORM="windows"; TOOLCHAIN="msvc" ;;
esac
else
ARCH="x64"
PLATFORM="linux"
TOOLCHAIN="gnu"
if [[ "${{ matrix.os }}" == windows-* ]]; then
PLATFORM="windows"
TOOLCHAIN="msvc"
elif [[ "${{ matrix.os }}" == macos-* ]]; then
ARCH="arm64"
PLATFORM="macos"
TOOLCHAIN="clang"
fi
ARCH="x64"
PLATFORM="linux"
TOOLCHAIN="gnu"
if [[ "${{ matrix.os }}" == windows-* ]]; then
PLATFORM="windows"
TOOLCHAIN="msvc"
elif [[ "${{ matrix.os }}" == macos-* ]]; then
ARCH="arm64"
PLATFORM="macos"
TOOLCHAIN="clang"
fi
SUFFIX=""
if [[ "${{ matrix.lto }}" == "ON" ]]; then
SUFFIX="-lto"
fi
if [[ "${{ matrix.llvm_mode }}" == "Debug" && "${{ matrix.os }}" != windows-* ]]; then
if [[ "${{ matrix.llvm_mode }}" == "Debug" ]]; then
SUFFIX="${SUFFIX}-asan"
fi
@@ -313,134 +189,3 @@ jobs:
name: ${{ env.LLVM_INSTALL_ARCHIVE }}
path: ${{ env.LLVM_INSTALL_ARCHIVE }}
if-no-files-found: error
test-cross:
needs: build
strategy:
fail-fast: false
matrix:
include:
- os: macos-15-intel
llvm_mode: RelWithDebInfo
target_triple: x86_64-apple-darwin
- os: ubuntu-24.04-arm
llvm_mode: RelWithDebInfo
target_triple: aarch64-linux-gnu
- os: windows-11-arm
llvm_mode: RelWithDebInfo
target_triple: aarch64-pc-windows-msvc
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: ./.github/actions/setup-pixi
with:
environments: test-run
- name: Download cross-compiled clice
uses: actions/download-artifact@v4
with:
name: cross-clice-${{ matrix.target_triple }}-${{ matrix.llvm_mode }}
path: build/${{ matrix.llvm_mode }}/
- name: Make binaries executable
if: runner.os != 'Windows'
run: chmod +x build/${{ matrix.llvm_mode }}/bin/*
- name: Run tests
run: pixi run -e test-run test ${{ matrix.llvm_mode }}
upload:
needs: build
if: ${{ !cancelled() && inputs.llvm_version && !inputs.skip_upload }}
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Download all build artifacts
env:
GH_TOKEN: ${{ github.token }}
run: scripts/download-llvm.sh "${{ github.run_id }}"
- name: Upload to clice-llvm
env:
GH_TOKEN: ${{ secrets.UPLOAD_LLVM }}
TARGET_REPO: clice-io/clice-llvm
run: python3 scripts/upload-llvm.py "${{ inputs.llvm_version }}" "${TARGET_REPO}" "${{ github.run_id }}"
- name: Save manifest for update-clice job
uses: actions/upload-artifact@v4
with:
name: llvm-manifest-final
path: artifacts/llvm-manifest.json
if-no-files-found: error
compression-level: 0
update-clice:
needs: upload
if: ${{ !inputs.skip_pr }}
runs-on: ubuntu-24.04
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Download manifest
uses: actions/download-artifact@v4
with:
name: llvm-manifest-final
path: .
- name: Update manifest and version
run: |
python3 scripts/update-llvm-version.py \
--version "${{ inputs.llvm_version }}" \
--manifest-src llvm-manifest.json \
--manifest-dest config/llvm-manifest.json \
--package-cmake cmake/package.cmake
- name: Create or update PR
env:
GH_TOKEN: ${{ github.token }}
run: |
VERSION="${{ inputs.llvm_version }}"
BRANCH="chore/update-llvm-${VERSION}"
RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
RELEASE_URL="https://github.com/clice-io/clice-llvm/releases/tag/${VERSION}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b "${BRANCH}"
git add config/llvm-manifest.json cmake/package.cmake
git commit -m "chore: update LLVM to ${VERSION}"
git push --force-with-lease origin "${BRANCH}"
# Check if PR already exists for this branch
EXISTING_PR=$(gh pr list --head "${BRANCH}" --json number --jq '.[0].number // empty')
BODY="$(cat <<EOF
## Summary
- Update LLVM prebuilt binaries to version ${VERSION}
- Updated \`config/llvm-manifest.json\` with new SHA256 hashes
- Updated \`cmake/package.cmake\` version string
**Artifacts:** [clice-llvm release](${RELEASE_URL})
**Build:** [workflow run](${RUN_URL})
> Auto-generated by build-llvm workflow
EOF
)"
if [[ -n "${EXISTING_PR}" ]]; then
echo "Updating existing PR #${EXISTING_PR}"
gh pr edit "${EXISTING_PR}" --body "${BODY}"
else
gh pr create \
--title "chore: update LLVM to ${VERSION}" \
--body "${BODY}" \
--base main
fi

View File

@@ -14,12 +14,6 @@ jobs:
with:
environments: format
- name: Validate update-llvm-version.py can still patch package.cmake
run: |
python3 scripts/update-llvm-version.py --check \
--manifest-dest config/llvm-manifest.json \
--package-cmake cmake/package.cmake
- name: Run formatter
run: pixi run format
continue-on-error: true

View File

@@ -7,10 +7,6 @@ on:
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changes:
if: ${{ !startsWith(github.ref, 'refs/tags/') }}

View File

@@ -9,7 +9,6 @@ jobs:
fail-fast: false
matrix:
include:
# Native builds
- os: windows-2025
artifact_name: clice.zip
asset_name: clice-x64-windows-msvc.zip
@@ -28,31 +27,6 @@ jobs:
symbol_artifact_name: clice-symbol.tar.gz
symbol_asset_name: clice-arm64-macos-darwin-symbol.tar.gz
# Cross-compilation builds
- os: macos-15
target_triple: x86_64-apple-darwin
pixi_env: cross-macos-x64
artifact_name: clice.tar.gz
asset_name: clice-x86_64-macos-darwin.tar.gz
symbol_artifact_name: clice-symbol.tar.gz
symbol_asset_name: clice-x86_64-macos-darwin-symbol.tar.gz
- os: ubuntu-24.04
target_triple: aarch64-linux-gnu
pixi_env: cross-linux-aarch64
artifact_name: clice.tar.gz
asset_name: clice-aarch64-linux-gnu.tar.gz
symbol_artifact_name: clice-symbol.tar.gz
symbol_asset_name: clice-aarch64-linux-gnu-symbol.tar.gz
- os: windows-2025
target_triple: aarch64-pc-windows-msvc
pixi_env: cross-windows-arm64
artifact_name: clice.zip
asset_name: clice-aarch64-windows-msvc.zip
symbol_artifact_name: clice-symbol.zip
symbol_asset_name: clice-aarch64-windows-msvc-symbol.zip
runs-on: ${{ matrix.os }}
defaults:
@@ -65,20 +39,11 @@ jobs:
- uses: ./.github/actions/setup-pixi
with:
environments: ${{ matrix.pixi_env || 'package' }}
environments: package
- name: Package (native)
if: ${{ !matrix.target_triple }}
- name: Package
run: pixi run package
- name: Package (cross-compile)
if: ${{ matrix.target_triple }}
run: |
ENV="${{ matrix.pixi_env }}"
pixi run -e "$ENV" package-config -- \
"-DCLICE_TARGET_TRIPLE=${{ matrix.target_triple }}"
pixi run -e "$ENV" cmake-build
- name: Upload Main Package to Release
if: startsWith(github.ref, 'refs/tags/v')
uses: svenstaro/upload-release-action@v2

View File

@@ -17,154 +17,53 @@ jobs:
strategy:
fail-fast: false
matrix:
include:
# Native builds
- os: windows-2025
build_type: RelWithDebInfo
- os: ubuntu-24.04
build_type: Debug
- os: ubuntu-24.04
build_type: RelWithDebInfo
- os: macos-15
build_type: Debug
- os: macos-15
build_type: RelWithDebInfo
# Cross-compile (build only; tests run on native runners)
- os: macos-15
build_type: RelWithDebInfo
target_triple: x86_64-apple-darwin
build_only: true
- os: ubuntu-24.04
build_type: RelWithDebInfo
target_triple: aarch64-linux-gnu
build_only: true
pixi_env: cross-linux-aarch64
- os: windows-2025
build_type: RelWithDebInfo
target_triple: aarch64-pc-windows-msvc
build_only: true
pixi_env: cross-windows-arm64
os: [windows-2025, ubuntu-24.04, macos-15]
build_type: [Debug, RelWithDebInfo]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: ./.github/actions/setup-pixi
with:
environments: ${{ matrix.pixi_env || 'default' }}
- name: Restore compiler cache
uses: actions/cache@v4
with:
path: ${{ runner.os == 'Windows' && '.cache/sccache' || '.cache/ccache' }}
key: ${{ runner.os }}-${{ matrix.build_type }}-${{ matrix.target_triple || 'native' }}-ccache-${{ github.sha }}
key: ${{ runner.os }}-${{ matrix.build_type }}-ccache-${{ github.sha }}
restore-keys: |
${{ runner.os }}-${{ matrix.build_type }}-${{ matrix.target_triple || 'native' }}-ccache-
${{ runner.os }}-${{ matrix.build_type }}-ccache-
- name: Zero cache stats
run: |
ENV="${{ matrix.pixi_env || 'default' }}"
if [ "$RUNNER_OS" = "Windows" ]; then
pixi run -e "$ENV" -- sccache --stop-server || true
pixi run -e "$ENV" -- sccache --zero-stats || true
pixi run -- sccache --stop-server || true
pixi run -- sccache --zero-stats || true
else
pixi run -e "$ENV" -- ccache --zero-stats || true
pixi run -- ccache --zero-stats || true
fi
shell: bash
- name: Build (native)
if: ${{ !matrix.target_triple }}
- name: Build
run: pixi run build ${{ matrix.build_type }} ON
- name: Build (cross-compile)
if: ${{ matrix.target_triple }}
shell: bash
run: |
ENV="${{ matrix.pixi_env || 'default' }}"
pixi run -e "$ENV" cmake-config ${{ matrix.build_type }} OFF -- \
"-DCLICE_TARGET_TRIPLE=${{ matrix.target_triple }}"
pixi run -e "$ENV" cmake-build ${{ matrix.build_type }}
- name: Upload cross-compiled binaries
if: ${{ matrix.build_only }}
uses: actions/upload-artifact@v4
with:
name: cross-build-${{ matrix.target_triple }}
path: |
build/${{ matrix.build_type }}/bin/
build/${{ matrix.build_type }}/lib/
if-no-files-found: error
retention-days: 1
- name: Unit tests
if: ${{ !matrix.build_only }}
timeout-minutes: 5
- name: Unit Test
run: pixi run unit-test ${{ matrix.build_type }}
- name: Integration tests
if: ${{ !matrix.build_only }}
timeout-minutes: 20
- name: Integration Test
run: pixi run integration-test ${{ matrix.build_type }}
- name: Smoke tests
if: ${{ !matrix.build_only }}
timeout-minutes: 15
- name: Smoke Test
if: success() || failure()
run: pixi run smoke-test ${{ matrix.build_type }}
- name: Print cache stats and stop server
if: always()
run: |
ENV="${{ matrix.pixi_env || 'default' }}"
if [ "$RUNNER_OS" = "Windows" ]; then
pixi run -e "$ENV" -- sccache --show-stats
pixi run -e "$ENV" -- sccache --stop-server || true
pixi run -- sccache --show-stats
pixi run -- sccache --stop-server || true
else
pixi run -e "$ENV" -- ccache --show-stats
pixi run -- ccache --show-stats
fi
shell: bash
test-cross:
needs: build
strategy:
fail-fast: false
matrix:
include:
- os: macos-15-intel
build_type: RelWithDebInfo
target_triple: x86_64-apple-darwin
- os: ubuntu-24.04-arm
build_type: RelWithDebInfo
target_triple: aarch64-linux-gnu
- os: windows-11-arm
build_type: RelWithDebInfo
target_triple: aarch64-pc-windows-msvc
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: ./.github/actions/setup-pixi
with:
environments: test-run
- name: Download cross-compiled binaries
uses: actions/download-artifact@v4
with:
name: cross-build-${{ matrix.target_triple }}
path: build/${{ matrix.build_type }}/
- name: Make binaries executable
if: runner.os != 'Windows'
run: chmod +x build/${{ matrix.build_type }}/bin/*
- name: Unit tests
timeout-minutes: 5
run: pixi run -e test-run unit-test ${{ matrix.build_type }}
- name: Integration tests
timeout-minutes: 20
run: pixi run -e test-run integration-test ${{ matrix.build_type }}
- name: Smoke tests
timeout-minutes: 10
run: pixi run -e test-run smoke-test ${{ matrix.build_type }}

View File

@@ -127,16 +127,9 @@ endif()
set(FBS_SCHEMA_FILE "${PROJECT_SOURCE_DIR}/src/index/schema.fbs")
set(GENERATED_HEADER "${PROJECT_BINARY_DIR}/generated/schema_generated.h")
if(CMAKE_CROSSCOMPILING)
find_program(FLATC_EXECUTABLE flatc REQUIRED)
set(FLATC_CMD "${FLATC_EXECUTABLE}")
else()
set(FLATC_CMD "$<TARGET_FILE:flatc>")
endif()
add_custom_command(
OUTPUT "${GENERATED_HEADER}"
COMMAND ${FLATC_CMD} --cpp -o "${PROJECT_BINARY_DIR}/generated" "${FBS_SCHEMA_FILE}"
COMMAND $<TARGET_FILE:flatc> --cpp -o "${PROJECT_BINARY_DIR}/generated" "${FBS_SCHEMA_FILE}"
DEPENDS "${FBS_SCHEMA_FILE}"
COMMENT "Generating C++ header from ${FBS_SCHEMA_FILE}"
)
@@ -158,13 +151,13 @@ target_link_libraries(clice-core PUBLIC
spdlog::spdlog
roaring::roaring
flatbuffers
kota::ipc::lsp
kota::codec::toml
eventide::ipc::lsp
eventide::serde::toml
simdjson::simdjson
)
add_executable(clice "${PROJECT_SOURCE_DIR}/src/clice.cc")
target_link_libraries(clice PRIVATE clice::core kota::deco)
target_link_libraries(clice PRIVATE clice::core eventide::deco)
install(TARGETS clice RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
add_custom_target(copy_clang_resource ALL
@@ -196,7 +189,7 @@ if(CLICE_ENABLE_TEST)
"${PROJECT_SOURCE_DIR}/src"
"${PROJECT_SOURCE_DIR}/tests/unit"
)
target_link_libraries(unit_tests PRIVATE clice::core kota::zest kota::deco)
target_link_libraries(unit_tests PRIVATE clice::core eventide::zest eventide::deco)
endif()
if(CLICE_ENABLE_BENCHMARK)
@@ -206,7 +199,7 @@ if(CLICE_ENABLE_BENCHMARK)
target_include_directories(scan_benchmark PRIVATE
"${PROJECT_SOURCE_DIR}/src"
)
target_link_libraries(scan_benchmark PRIVATE clice::core kota::deco)
target_link_libraries(scan_benchmark PRIVATE clice::core eventide::deco)
endif()
if(CLICE_RELEASE)

View File

@@ -21,15 +21,17 @@
#include <thread>
#include "command/command.h"
#include "eventide/deco/deco.h"
#include "eventide/serde/json/serializer.h"
#include "support/filesystem.h"
#include "support/logging.h"
#include "support/path_pool.h"
#include "syntax/dependency_graph.h"
#include "kota/codec/json/json.h"
#include "kota/deco/deco.h"
#include "llvm/Support/FileSystem.h"
namespace et = eventide;
using namespace clice;
struct BenchmarkOptions {
@@ -95,7 +97,7 @@ void export_graph_json(const PathPool& path_pool,
export_data.files.push_back(std::move(node));
}
auto json = kota::codec::json::to_json(export_data);
auto json = et::serde::json::to_json(export_data);
if(!json) {
std::println(stderr, "Failed to serialize dependency graph");
return;
@@ -219,8 +221,8 @@ void print_report(const ScanReport& report) {
}
int main(int argc, const char** argv) {
auto args = kota::deco::util::argvify(argc, argv);
auto result = kota::deco::cli::parse<BenchmarkOptions>(args);
auto args = deco::util::argvify(argc, argv);
auto result = deco::cli::parse<BenchmarkOptions>(args);
if(!result.has_value()) {
std::println(stderr, "Error: {}", result.error().message);
@@ -231,7 +233,7 @@ int main(int argc, const char** argv) {
if(opts.help.value_or(false) || !opts.cdb_path.has_value()) {
std::ostringstream oss;
kota::deco::cli::write_usage_for<BenchmarkOptions>(oss, "scan_benchmark [OPTIONS] <cdb>");
deco::cli::write_usage_for<BenchmarkOptions>(oss, "scan_benchmark [OPTIONS] <cdb>");
std::print("{}", oss.str());
return opts.help.value_or(false) ? 0 : 1;
}

View File

@@ -25,22 +25,6 @@ function(setup_llvm LLVM_VERSION)
list(APPEND LLVM_SETUP_ARGS "--offline")
endif()
if(DEFINED CLICE_TARGET_TRIPLE)
if(CLICE_TARGET_TRIPLE MATCHES "linux")
list(APPEND LLVM_SETUP_ARGS "--target-platform" "Linux")
elseif(CLICE_TARGET_TRIPLE MATCHES "darwin")
list(APPEND LLVM_SETUP_ARGS "--target-platform" "macosx")
elseif(CLICE_TARGET_TRIPLE MATCHES "windows")
list(APPEND LLVM_SETUP_ARGS "--target-platform" "Windows")
endif()
if(CLICE_TARGET_TRIPLE MATCHES "^aarch64")
list(APPEND LLVM_SETUP_ARGS "--target-arch" "arm64")
elseif(CLICE_TARGET_TRIPLE MATCHES "^x86_64")
list(APPEND LLVM_SETUP_ARGS "--target-arch" "x64")
endif()
endif()
execute_process(
COMMAND "${Python3_EXECUTABLE}" "${LLVM_SETUP_SCRIPT}" ${LLVM_SETUP_ARGS}
RESULT_VARIABLE LLVM_SETUP_RESULT

View File

@@ -1,7 +1,7 @@
include_guard()
include(${CMAKE_CURRENT_LIST_DIR}/llvm.cmake)
setup_llvm("21.1.8")
setup_llvm("21.1.4+r1")
# install dependencies
include(FetchContent)
@@ -39,18 +39,18 @@ set(FLATBUFFERS_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(FLATBUFFERS_BUILD_FLATHASH OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
kotatsu
GIT_REPOSITORY https://github.com/clice-io/kotatsu
eventide
GIT_REPOSITORY https://github.com/clice-io/eventide
GIT_TAG main
GIT_SHALLOW TRUE
)
set(KOTA_ENABLE_ZEST ON)
set(KOTA_ENABLE_TEST OFF)
set(KOTA_CODEC_ENABLE_SIMDJSON ON)
set(KOTA_CODEC_ENABLE_YYJSON ON)
set(KOTA_CODEC_ENABLE_TOML ON)
set(KOTA_ENABLE_EXCEPTIONS OFF)
set(KOTA_ENABLE_RTTI OFF)
set(ETD_ENABLE_ZEST ON)
set(ETD_ENABLE_TEST OFF)
set(ETD_SERDE_ENABLE_SIMDJSON ON)
set(ETD_SERDE_ENABLE_YYJSON ON)
set(ETD_SERDE_ENABLE_TOML ON)
set(ETD_ENABLE_EXCEPTIONS OFF)
set(ETD_ENABLE_RTTI OFF)
FetchContent_MakeAvailable(kotatsu spdlog croaring flatbuffers)
FetchContent_MakeAvailable(eventide spdlog croaring flatbuffers)

View File

@@ -1,29 +1,5 @@
cmake_minimum_required(VERSION 3.30)
# Cross-compilation support via CLICE_TARGET_TRIPLE.
# Examples:
# -DCLICE_TARGET_TRIPLE=x86_64-apple-darwin (macOS x64 from arm64)
# -DCLICE_TARGET_TRIPLE=aarch64-linux-gnu (Linux arm64 from x64)
# -DCLICE_TARGET_TRIPLE=aarch64-pc-windows-msvc (Windows arm64 from x64)
if(DEFINED CLICE_TARGET_TRIPLE)
if(CLICE_TARGET_TRIPLE MATCHES "^x86_64-apple-darwin")
set(CMAKE_OSX_ARCHITECTURES "x86_64" CACHE STRING "")
elseif(CLICE_TARGET_TRIPLE MATCHES "^aarch64-.*linux")
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER_TARGET "aarch64-linux-gnu" CACHE STRING "")
set(CMAKE_CXX_COMPILER_TARGET "aarch64-linux-gnu" CACHE STRING "")
if(DEFINED ENV{CONDA_PREFIX} AND NOT DEFINED CMAKE_SYSROOT)
set(CMAKE_SYSROOT "$ENV{CONDA_PREFIX}/aarch64-conda-linux-gnu/sysroot" CACHE PATH "")
endif()
elseif(CLICE_TARGET_TRIPLE MATCHES "^aarch64-.*-windows")
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR ARM64)
set(CMAKE_C_COMPILER_TARGET "aarch64-pc-windows-msvc" CACHE STRING "")
set(CMAKE_CXX_COMPILER_TARGET "aarch64-pc-windows-msvc" CACHE STRING "")
endif()
endif()
set(CMAKE_C_COMPILER clang CACHE STRING "")
set(CMAKE_CXX_COMPILER clang++ CACHE STRING "")

View File

@@ -1,142 +1,83 @@
[
{
"version": "21.1.8",
"filename": "aarch64-linux-gnu-releasedbg-lto.tar.xz",
"sha256": "f3444ee840b50933c23656cbee7c4d010e752ac55ca66095b97f7c0e997b13b5",
"lto": true,
"asan": false,
"platform": "linux",
"arch": "arm64",
"build_type": "RelWithDebInfo"
},
{
"version": "21.1.8",
"filename": "aarch64-linux-gnu-releasedbg.tar.xz",
"sha256": "b9012bf059e4d8673fb564b5780e5fc78c6a2e47f5cc6a39f444d1879b42dd2a",
"lto": false,
"asan": false,
"platform": "linux",
"arch": "arm64",
"build_type": "RelWithDebInfo"
},
{
"version": "21.1.8",
"filename": "aarch64-windows-msvc-releasedbg-lto.tar.xz",
"sha256": "8870d16141ba7f9ea12f5147b8d91329abbbaa4376cd4576667dd323d896dd08",
"lto": true,
"asan": false,
"platform": "windows",
"arch": "arm64",
"build_type": "RelWithDebInfo"
},
{
"version": "21.1.8",
"filename": "aarch64-windows-msvc-releasedbg.tar.xz",
"sha256": "ad394e79ec85dd40f942671bb0342ffe54a103eb2baabacb773999d57d80134b",
"lto": false,
"asan": false,
"platform": "windows",
"arch": "arm64",
"build_type": "RelWithDebInfo"
},
{
"version": "21.1.8",
"version": "21.1.4+r1",
"filename": "arm64-macos-clang-debug-asan.tar.xz",
"sha256": "b02d20e4f7294ee33f49a09dfdd765b3b44135e003ef50e3a760aeee39e3f993",
"sha256": "7da4b7d63edefecaf11773e7e701c575140d1a07329bbbb038673b6ee4516ff5",
"lto": false,
"asan": true,
"platform": "macosx",
"arch": "arm64",
"build_type": "Debug"
},
{
"version": "21.1.8",
"version": "21.1.4+r1",
"filename": "arm64-macos-clang-releasedbg-lto.tar.xz",
"sha256": "e40c21eb0d0b91d9d4ab31212a5cb01ea46707f5c29839414567857e4147604d",
"sha256": "300455b169448f9f01ae95e3bc269f489558a4ca3955e3032171cc75feca0e30",
"lto": true,
"asan": false,
"platform": "macosx",
"arch": "arm64",
"build_type": "RelWithDebInfo"
},
{
"version": "21.1.8",
"version": "21.1.4+r1",
"filename": "arm64-macos-clang-releasedbg.tar.xz",
"sha256": "e1b01de34f0edfd41c118e4981a93afb35556ae369597e864f4a393db623b926",
"sha256": "9abfc6cd65b957d734ffb97610a634fb4a66d3fbe0fcfb5a1c9124ef693c1495",
"lto": false,
"asan": false,
"platform": "macosx",
"arch": "arm64",
"build_type": "RelWithDebInfo"
},
{
"version": "21.1.8",
"version": "21.1.4+r1",
"filename": "x64-linux-gnu-debug-asan.tar.xz",
"sha256": "76bb82d822b5377fb5e0fac8abcfba125142e6a0acc02bb36d1fa1532a268646",
"sha256": "c1ad3ec476911596a842ac67dd9c9c9475ce9f0a77b81101d3c801840292e7bc",
"lto": false,
"asan": true,
"platform": "linux",
"arch": "x64",
"build_type": "Debug"
},
{
"version": "21.1.8",
"version": "21.1.4+r1",
"filename": "x64-linux-gnu-releasedbg-lto.tar.xz",
"sha256": "32f5edddec1e689124f045b586fb402ae30febc05203af7391b088bc8494cd53",
"sha256": "8a869c2184d139dbba704e2d712e7a68336458ad2d70622b3eb906c3e3511e54",
"lto": true,
"asan": false,
"platform": "linux",
"arch": "x64",
"build_type": "RelWithDebInfo"
},
{
"version": "21.1.8",
"version": "21.1.4+r1",
"filename": "x64-linux-gnu-releasedbg.tar.xz",
"sha256": "8ba3c84f23a2a81a86c54780754a61adf99048aa2ac0dc9b9708d0f842d553de",
"sha256": "552bab86f715d4f2c027f07eaaf5b3d6b8e430af0b74b470142f3f00da4feec6",
"lto": false,
"asan": false,
"platform": "linux",
"arch": "x64",
"build_type": "RelWithDebInfo"
},
{
"version": "21.1.8",
"filename": "x64-macos-clang-releasedbg-lto.tar.xz",
"sha256": "97e81d6296896d7237f118f728d05291707b9e4e5791e07ce4be8aee0517505d",
"lto": true,
"asan": false,
"platform": "macosx",
"arch": "x64",
"build_type": "RelWithDebInfo"
},
{
"version": "21.1.8",
"filename": "x64-macos-clang-releasedbg.tar.xz",
"sha256": "53c13f8e1082fa2fe2f9c05303de48cb3133bf5f24271f4b3062f1dec578159c",
"version": "21.1.4+r1",
"filename": "x64-windows-msvc-debug-asan.tar.xz",
"sha256": "093667a493d336c22ff3c604c5f1fea2a7d2c927c1179cec44e9a03726906ac1",
"lto": false,
"asan": false,
"platform": "macosx",
"arch": "x64",
"build_type": "RelWithDebInfo"
"asan": true,
"platform": "windows",
"build_type": "Debug"
},
{
"version": "21.1.8",
"version": "21.1.4+r1",
"filename": "x64-windows-msvc-releasedbg-lto.tar.xz",
"sha256": "16bcf0e4cbc3d2b1204edd619a3837004dacea28eeff0a101c8d0212f936427d",
"sha256": "010539e85621dc3c6ecf359d899feb4075aeca5d0bba6625cdbec0e570e79129",
"lto": true,
"asan": false,
"platform": "windows",
"arch": "x64",
"build_type": "RelWithDebInfo"
},
{
"version": "21.1.8",
"version": "21.1.4+r1",
"filename": "x64-windows-msvc-releasedbg.tar.xz",
"sha256": "81d31fad05e200726c8178314b0b2045c947483dddd8cb974f4c376ae5f441fa",
"sha256": "f473c09fbea10053fac00be409d75dc228d4a38bcbc5e4aeb58b56a4b0dde78e",
"lto": false,
"asan": false,
"platform": "windows",
"arch": "x64",
"build_type": "RelWithDebInfo"
}
]

View File

@@ -91,7 +91,7 @@ The worker pool (`src/server/worker_pool.cpp`) manages spawning and communicatin
### Communication
Workers communicate with the master via **stdio pipes** using a **bincode** serialization format (via `kota::ipc::BincodePeer`). This is more compact and faster than JSON for internal IPC, while the master handles JSON for the external LSP protocol.
Workers communicate with the master via **stdio pipes** using a **bincode** serialization format (via `eventide::ipc::BincodePeer`). This is more compact and faster than JSON for internal IPC, while the master handles JSON for the external LSP protocol.
### Stateful Worker Routing
@@ -111,7 +111,7 @@ The stateful worker (`src/server/stateful_worker.cpp`) caches compiled ASTs in m
- **Feature queries**: Look up the cached AST and invoke the corresponding `feature::*` function (hover, semantic tokens, etc.), serializing the result to JSON
- **Document updates**: Received as notifications — the worker updates the stored text and marks the document as `dirty`, causing feature queries to return `null` until recompilation
- **Eviction**: LRU-based; evicts the oldest document when capacity is exceeded, notifying the master
- **Concurrency**: Each document has a per-document `kota::mutex` (strand) to serialize compilation and feature queries. Heavy work (compilation, feature extraction) runs on a thread pool via `kota::queue`.
- **Concurrency**: Each document has a per-document `et::mutex` (strand) to serialize compilation and feature queries. Heavy work (compilation, feature extraction) runs on a thread pool via `et::queue`.
## Stateless Worker
@@ -123,7 +123,7 @@ The stateless worker (`src/server/stateless_worker.cpp`) handles one-shot reques
- **Build PCM**: Compiles a C++20 module interface to a temporary file
- **Index**: Compiles a file for indexing (TUIndex generation — currently a stub)
All requests are dispatched to a thread pool via `kota::queue`.
All requests are dispatched to a thread pool via `et::queue`.
## Compile Graph
@@ -132,7 +132,7 @@ The compile graph (`src/server/compile_graph.cpp`) tracks compilation unit depen
- **Registration**: Each file registers its included dependencies
- **Cascade invalidation**: When a file changes, all transitive dependents are marked dirty and their ongoing compilations are cancelled
- **Dependency compilation**: Before compiling a file, `compile_deps` ensures all dependencies (PCH, PCMs) are built first
- **Cancellation**: Uses `kota::cancellation_source` to abort in-flight compilations when files are invalidated
- **Cancellation**: Uses `et::cancellation_source` to abort in-flight compilations when files are invalidated
## Configuration

5486
pixi.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,24 +14,17 @@ readme = "README.md"
documentation = "https://docs.clice.io/clice/"
repository = "https://github.com/clice-io/clice"
channels = ["conda-forge"]
platforms = ["win-64", "linux-64", "osx-arm64", "osx-64", "linux-aarch64", "win-arm64"]
platforms = ["win-64", "linux-64", "osx-arm64"]
[environments]
default = ["build", "test"]
package = ["build", "test", "package"]
cross-macos-x64 = ["build", "package", "cross-macos-x64"]
cross-linux-aarch64 = ["build", "package", "cross-linux-aarch64"]
cross-windows-arm64 = ["build", "package", "cross-windows-arm64"]
node = ["node"]
format = ["format"]
test-run = ["test"]
# ============================================================================== #
# DEPENDENCIES #
# ============================================================================== #
[feature.build]
platforms = ["win-64", "linux-64", "osx-arm64", "osx-64", "linux-aarch64"]
[feature.build.dependencies]
python = ">=3.13"
cmake = ">=3.30"
@@ -42,7 +35,6 @@ lld = "==20.1.8"
llvm-tools = "==20.1.8"
clang-tools = "==20.1.8"
compiler-rt = "==20.1.8"
flatbuffers = "==25.9.23"
[feature.build.target.win-64.dependencies]
sccache = "*"
@@ -62,68 +54,23 @@ scripts = ["scripts/activate_linux.sh"]
[feature.build.target.win-64.activation]
scripts = ["scripts/activate_asan.bat"]
# macOS x64 (from arm64): clang natively supports cross-arch, no extra deps.
[feature.cross-macos-x64.target.osx-arm64.dependencies]
[feature.cross-macos-x64.target.osx-arm64.activation]
scripts = ["scripts/activate_cross_macos.sh"]
# Linux aarch64 (from x64): needs aarch64 sysroot and cross gcc for libstdc++.
[feature.cross-linux-aarch64.target.linux-64.dependencies]
sysroot_linux-aarch64 = "==2.17"
gcc_linux-aarch64 = "==14.2.0"
gxx_linux-aarch64 = "==14.2.0"
[feature.cross-linux-aarch64.target.linux-64.activation]
scripts = ["scripts/activate_cross_linux.sh"]
# Windows arm64 (from x64): Windows SDK on CI already includes ARM64 libs.
[feature.cross-windows-arm64.target.win-64.dependencies]
[feature.cross-windows-arm64.target.win-64.activation]
scripts = ["scripts/activate_cross_windows.bat"]
[feature.test.dependencies]
python = ">=3.13"
# On macOS, the system Apple clang emits vendor-specific flags that upstream
# LLVM cannot parse. Providing upstream clang + lld in PATH prevents
# fallback to /usr/bin/clang++ and satisfies toolchain.cmake's -fuse-ld=lld.
[feature.test.target.osx-64.dependencies]
clang = "==20.1.8"
clangxx = "==20.1.8"
lld = "==20.1.8"
[feature.test.target.osx-arm64.dependencies]
clang = "==20.1.8"
clangxx = "==20.1.8"
lld = "==20.1.8"
[feature.test.pypi-dependencies]
pytest = "*"
pytest-asyncio = ">=1.1.0"
pytest-timeout = "*"
pygls = ">=2.0.0"
lsprotocol = ">=2024.0.0"
[feature.package.dependencies]
xz = ">=5.8.1,<6"
[feature.package.tasks.package-config]
args = [{ arg = "type", default = "RelWithDebInfo" }]
cmd = """
cmake -B build/{{ type }} -G Ninja \
-DCMAKE_BUILD_TYPE={{ type }} \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchain.cmake \
-DCLICE_RELEASE=ON
"""
[feature.package.tasks.package]
args = [{ arg = "type", default = "RelWithDebInfo" }]
depends-on = [
{ task = "package-config", args = ["{{ type }}"] },
{ task = "cmake-build", args = ["{{ type }}"] },
]
cmd = """
cmake -B build/RelWithDebInfo -G Ninja \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchain.cmake \
-DCLICE_RELEASE=ON && \
cmake --build build/RelWithDebInfo
"""
# ============================================================================== #
# CMAKE #
@@ -132,13 +79,14 @@ depends-on = [
args = [
{ arg = "type", default = "RelWithDebInfo" },
{ arg = "ci", default = "OFF" },
{ arg = "extra", default = "" },
]
cmd = """
cmake -B build/{{ type }} -G Ninja \
-DCMAKE_BUILD_TYPE={{ type }} \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchain.cmake \
-DCLICE_ENABLE_TEST=ON \
-DCLICE_CI_ENVIRONMENT={{ ci }}
-DCLICE_CI_ENVIRONMENT={{ ci }} {{extra}} \
"""
[feature.build.tasks.cmake-build]
@@ -149,9 +97,10 @@ cmd = "cmake --build build/{{ type }}"
args = [
{ arg = "type", default = "RelWithDebInfo" },
{ arg = "ci", default = "OFF" },
{ arg = "extra", default = "" },
]
depends-on = [
{ task = "cmake-config", args = ["{{ type }}", "{{ ci }}"] },
{ task = "cmake-config", args = ["{{ type }}", "{{ ci }}", "{{extra}}"] },
{ task = "cmake-build", args = ["{{ type }}"] },
]
@@ -159,15 +108,15 @@ depends-on = [
args = [{ arg = "type", default = "RelWithDebInfo" }]
depends-on = [{ task = "lint-cpp", args = ["{{ type }}"] }]
[feature.test.tasks.unit-test]
[feature.build.tasks.unit-test]
args = [{ arg = "type", default = "RelWithDebInfo" }]
cmd = './build/{{ type }}/bin/unit_tests --test-dir="./tests/data"'
[feature.test.tasks.integration-test]
args = [{ arg = "type", default = "RelWithDebInfo" }]
cmd = """
pytest -s --log-cli-level=INFO --timeout=300 --timeout-method=thread \
tests/integration --executable=./build/{{ type }}/bin/clice
pytest -s --log-cli-level=INFO tests/integration \
--executable=./build/{{ type }}/bin/clice
"""
[feature.test.tasks.smoke-test]
@@ -182,7 +131,6 @@ args = [{ arg = "type", default = "RelWithDebInfo" }]
depends-on = [
{ task = "unit-test", args = ["{{ type }}"] },
{ task = "integration-test", args = ["{{ type }}"] },
{ task = "smoke-test", args = ["{{ type }}"] },
]
# ============================================================================== #
@@ -204,14 +152,9 @@ gh workflow run upload-llvm.yml \
args = ["file_name"]
cmd = ["scripts/delete-artifacts.bash", "{{ file_name }}"]
[dependencies]
# ============================================================================== #
# DOCS & VSCODE EXTENSION #
# ============================================================================== #
[feature.node]
platforms = ["win-64", "linux-64", "osx-arm64", "osx-64", "linux-aarch64"]
[feature.node.dependencies]
nodejs = ">=20"
pnpm = "*"
@@ -237,9 +180,6 @@ outputs = ["editors/vscode/node_modules/.modules.yaml"]
# ============================================================================== #
# FORMAT #
# ============================================================================== #
[feature.format]
platforms = ["win-64", "linux-64", "osx-arm64", "osx-64", "linux-aarch64"]
[feature.format.dependencies]
ruff = "*"
tombi = "*"

View File

@@ -1,12 +0,0 @@
#!/bin/sh
# Clear conda cross-gcc flags so host x86_64 paths don't leak into the
# aarch64 build. conda's gcc_linux-aarch64 activation sets
# CFLAGS/CXXFLAGS/CPPFLAGS/LDFLAGS with -isystem/-L pointing at $CONDA_PREFIX
# (x86_64 host paths). LIBRARY_PATH from ld_impl_linux-64 likewise points at
# host libs. Empty-string export reliably overrides conda-installed values
# regardless of whether pixi sources or calls this script.
export CFLAGS=
export CXXFLAGS=
export CPPFLAGS=
export LDFLAGS=
export LIBRARY_PATH=

View File

@@ -1,8 +0,0 @@
#!/bin/sh
# Clear conda host flags so arm64 host paths don't leak into the x86_64-macos
# cross build. See scripts/activate_cross_linux.sh for rationale.
export CFLAGS=
export CXXFLAGS=
export CPPFLAGS=
export LDFLAGS=
export LIBRARY_PATH=

View File

@@ -1,8 +0,0 @@
@echo off
REM Clear conda host flags so host x64 paths don't leak into the aarch64-windows
REM cross build. See scripts/activate_cross_linux.sh for rationale.
set "CFLAGS="
set "CXXFLAGS="
set "CPPFLAGS="
set "LDFLAGS="
set "LIBRARY_PATH="

View File

@@ -4,7 +4,6 @@ import subprocess
import shutil
import argparse
import os
import json
from pathlib import Path
@@ -23,66 +22,6 @@ def normalize_mode(value: str) -> str:
)
def build_native_tools(project_root: Path, build_dir: Path) -> Path:
"""Build native host tablegen tools for cross-compilation.
When cross-compiling LLVM, build tools like llvm-tblgen must run on the
host but would otherwise be compiled for the target architecture. This
function performs a minimal native build and returns the bin directory
containing host-runnable executables.
"""
native_dir = build_dir.parent / f"{build_dir.name}-native-tools"
native_dir.mkdir(exist_ok=True)
source_dir = project_root / "llvm"
cmake_args = [
"-G",
"Ninja",
"-DCMAKE_BUILD_TYPE=Release",
"-DLLVM_ENABLE_PROJECTS=clang;clang-tools-extra",
"-DLLVM_TARGETS_TO_BUILD=Native",
"-DLLVM_DISABLE_ASSEMBLY_FILES=ON",
"-DCMAKE_C_FLAGS=-w",
"-DCMAKE_CXX_FLAGS=-w",
]
if sys.platform == "win32":
cmake_args += [
"-DCMAKE_C_COMPILER=clang-cl",
"-DCMAKE_CXX_COMPILER=clang-cl",
]
else:
cmake_args += [
"-DCMAKE_C_COMPILER=clang",
"-DCMAKE_CXX_COMPILER=clang++",
]
print(f"\nConfiguring native host tools in {native_dir}...")
subprocess.check_call(
["cmake", "-S", str(source_dir), "-B", str(native_dir)] + cmake_args
)
required_tools = ["llvm-tblgen", "llvm-min-tblgen", "clang-tblgen"]
optional_tools = ["clang-tidy-confusable-chars-gen"]
for tool in required_tools:
print(f"Building native {tool}...")
subprocess.check_call(["cmake", "--build", str(native_dir), "--target", tool])
for tool in optional_tools:
try:
print(f"Building native {tool} (optional)...")
subprocess.check_call(
["cmake", "--build", str(native_dir), "--target", tool]
)
except subprocess.CalledProcessError:
print(f" {tool} not available, skipping.")
bin_dir = native_dir / "bin"
print(f"Native host tools ready in {bin_dir}")
return bin_dir
def main():
parser = argparse.ArgumentParser(
description="Build LLVM with specific configurations."
@@ -109,10 +48,6 @@ def main():
"--build-dir",
help="Custom build directory (relative to project root or absolute)",
)
parser.add_argument(
"--target-triple",
help="Cross-compilation target triple (e.g. x86_64-apple-darwin, aarch64-linux-gnu, aarch64-pc-windows-msvc)",
)
args = parser.parse_args()
@@ -150,46 +85,118 @@ def main():
print("--- Configuration ---")
print(f"Mode: {args.mode}")
print(f"LTO: {args.lto}")
print(f"Target Triple: {args.target_triple or '(native)'}")
print(f"Root: {project_root}")
print(f"Build Dir: {build_dir}")
print(f"Install Prefix: {install_prefix}")
print(f"Toolchain: {toolchain_file}")
print("---------------------")
components_path = Path(__file__).resolve().parent / "llvm-components.json"
with components_path.open() as f:
llvm_distribution_components = json.load(f)["components"]
llvm_distribution_components = [
"LLVMDemangle",
"LLVMSupport",
"LLVMCore",
"LLVMOption",
"LLVMBinaryFormat",
"LLVMMC",
"LLVMMCParser",
"LLVMObject",
"LLVMProfileData",
"LLVMBitReader",
"LLVMBitstreamReader",
"LLVMRemarks",
"LLVMObjectYAML",
"LLVMAggressiveInstCombine",
"LLVMInstCombine",
"LLVMIRReader",
"LLVMTextAPI",
"LLVMSymbolize",
"LLVMDebugInfoDWARF",
"LLVMDebugInfoDWARFLowLevel",
"LLVMDebugInfoCodeView",
"LLVMDebugInfoGSYM",
"LLVMDebugInfoPDB",
"LLVMDebugInfoBTF",
"LLVMDebugInfoMSF",
"LLVMAsmParser",
"LLVMTargetParser",
"LLVMTransformUtils",
"LLVMAnalysis",
"LLVMScalarOpts",
"LLVMFrontendHLSL",
"LLVMFrontendOpenMP",
"LLVMFrontendOffloading",
"LLVMFrontendAtomic",
"LLVMFrontendDirective",
"LLVMWindowsDriver",
"clangIndex",
"clangAPINotes",
"clangAST",
"clangASTMatchers",
"clangBasic",
"clangDriver",
"clangFormat",
"clangFrontend",
"clangLex",
"clangParse",
"clangSema",
"clangSerialization",
"clangRewrite",
"clangAnalysis",
"clangEdit",
"clangSupport",
"clangStaticAnalyzerCore",
"clangStaticAnalyzerFrontend",
"clangTidy",
"clangTidyUtils",
"clangTidyAndroidModule",
"clangTidyAbseilModule",
"clangTidyAlteraModule",
"clangTidyBoostModule",
"clangTidyBugproneModule",
"clangTidyCERTModule",
"clangTidyConcurrencyModule",
"clangTidyCppCoreGuidelinesModule",
"clangTidyDarwinModule",
"clangTidyFuchsiaModule",
"clangTidyGoogleModule",
"clangTidyHICPPModule",
"clangTidyLinuxKernelModule",
"clangTidyLLVMModule",
"clangTidyLLVMLibcModule",
"clangTidyMiscModule",
"clangTidyModernizeModule",
"clangTidyObjCModule",
"clangTidyOpenMPModule",
"clangTidyPerformanceModule",
"clangTidyPortabilityModule",
"clangTidyReadabilityModule",
"clangTidyZirconModule",
"clangTooling",
"clangToolingCore",
"clangToolingInclusions",
"clangToolingInclusionsStdlib",
"clangToolingSyntax",
"clangToolingRefactoring",
"clangTransformer",
"clangCrossTU",
"clangAnalysisFlowSensitive",
"clangAnalysisFlowSensitiveModels",
"clangStaticAnalyzerCheckers",
"clangIncludeCleaner",
"llvm-headers",
"clang-headers",
"clang-tidy-headers",
"clang-resource-headers",
]
components_joined = ";".join(llvm_distribution_components)
cmake_args = [
"-G",
"Ninja",
f"-DCMAKE_TOOLCHAIN_FILE={toolchain_file.as_posix()}",
f"-DCMAKE_INSTALL_PREFIX={install_prefix}",
]
if sys.platform == "win32":
# Use clang-cl (MSVC driver) on Windows so that LLVM's CMake
# generates correct MSVC-style linker flags for LTO, etc.
c_flags = "-w"
if args.target_triple:
c_flags += f" --target={args.target_triple}"
cmake_args += [
"-DCMAKE_C_COMPILER=clang-cl",
"-DCMAKE_CXX_COMPILER=clang-cl",
f"-DCMAKE_C_FLAGS={c_flags}",
f"-DCMAKE_CXX_FLAGS={c_flags}",
"-DLLVM_USE_LINKER=lld-link",
]
else:
cmake_args += [
f"-DCMAKE_TOOLCHAIN_FILE={toolchain_file.as_posix()}",
"-DCMAKE_C_FLAGS=-w",
"-DCMAKE_CXX_FLAGS=-w",
"-DLLVM_USE_LINKER=lld",
]
cmake_args += [
"-DCMAKE_C_FLAGS=-w",
"-DCMAKE_CXX_FLAGS=-w",
"-DLLVM_ENABLE_ZLIB=OFF",
"-DLLVM_ENABLE_ZSTD=OFF",
"-DLLVM_ENABLE_LIBXML2=OFF",
@@ -224,6 +231,7 @@ def main():
"-DCMAKE_JOB_POOL_LINK=console",
"-DLLVM_ENABLE_PROJECTS=clang;clang-tools-extra",
"-DLLVM_TARGETS_TO_BUILD=all",
"-DLLVM_USE_LINKER=lld",
"-DLLVM_DISABLE_ASSEMBLY_FILES=ON",
# Distribution
f"-DLLVM_DISTRIBUTION_COMPONENTS={components_joined}",
@@ -248,10 +256,8 @@ def main():
is_shared = "OFF"
if args.mode == "Debug":
cmake_args.append("-DCMAKE_BUILD_TYPE=Debug")
# ASAN is incompatible with -MDd on Windows (clang-cl), skip it there.
if sys.platform != "win32":
cmake_args.append("-DLLVM_USE_SANITIZER=Address")
is_shared = "ON"
cmake_args.append("-DLLVM_USE_SANITIZER=Address")
is_shared = "ON"
elif args.mode == "Release":
cmake_args.append("-DCMAKE_BUILD_TYPE=Release")
elif args.mode == "RelWithDebInfo":
@@ -266,24 +272,6 @@ def main():
else:
cmake_args.append("-DLLVM_ENABLE_LTO=OFF")
if args.target_triple:
cmake_args.append(f"-DCLICE_TARGET_TRIPLE={args.target_triple}")
cmake_args.append(f"-DLLVM_HOST_TRIPLE={args.target_triple}")
# When cross-compiling, clear conda's host-platform flags so they
# don't leak into the target build (e.g. -L pointing to x86_64 libs).
# This must happen before the native-tools build too so we don't
# contaminate the native configure with target-arch link flags.
for var in ["LIBRARY_PATH", "LDFLAGS", "CFLAGS", "CXXFLAGS", "CPPFLAGS"]:
os.environ.pop(var, None)
# Cross-compilation needs native host tools (tablegen, etc.) that can
# run on the build machine. macOS handles this transparently via
# Rosetta 2, but Linux and Windows require a separate native build.
if sys.platform != "darwin":
native_bin_dir = build_native_tools(project_root, build_dir)
cmake_args.append(f"-DLLVM_NATIVE_TOOL_DIR={native_bin_dir}")
build_dir.mkdir(exist_ok=True)
print(f"\nConfiguring in {build_dir}...")

View File

@@ -1,99 +0,0 @@
{
"components": [
"LLVMDemangle",
"LLVMSupport",
"LLVMCore",
"LLVMOption",
"LLVMBinaryFormat",
"LLVMMC",
"LLVMMCParser",
"LLVMObject",
"LLVMProfileData",
"LLVMBitReader",
"LLVMBitstreamReader",
"LLVMRemarks",
"LLVMObjectYAML",
"LLVMAggressiveInstCombine",
"LLVMInstCombine",
"LLVMIRReader",
"LLVMTextAPI",
"LLVMSymbolize",
"LLVMDebugInfoDWARF",
"LLVMDebugInfoDWARFLowLevel",
"LLVMDebugInfoCodeView",
"LLVMDebugInfoGSYM",
"LLVMDebugInfoPDB",
"LLVMDebugInfoBTF",
"LLVMDebugInfoMSF",
"LLVMAsmParser",
"LLVMTargetParser",
"LLVMTransformUtils",
"LLVMAnalysis",
"LLVMScalarOpts",
"LLVMFrontendHLSL",
"LLVMFrontendOpenMP",
"LLVMFrontendOffloading",
"LLVMFrontendAtomic",
"LLVMFrontendDirective",
"LLVMWindowsDriver",
"clangIndex",
"clangAPINotes",
"clangAST",
"clangASTMatchers",
"clangBasic",
"clangDriver",
"clangFormat",
"clangFrontend",
"clangLex",
"clangParse",
"clangSema",
"clangSerialization",
"clangRewrite",
"clangAnalysis",
"clangEdit",
"clangSupport",
"clangStaticAnalyzerCore",
"clangStaticAnalyzerFrontend",
"clangTidy",
"clangTidyUtils",
"clangTidyAndroidModule",
"clangTidyAbseilModule",
"clangTidyAlteraModule",
"clangTidyBoostModule",
"clangTidyBugproneModule",
"clangTidyCERTModule",
"clangTidyConcurrencyModule",
"clangTidyCppCoreGuidelinesModule",
"clangTidyDarwinModule",
"clangTidyFuchsiaModule",
"clangTidyGoogleModule",
"clangTidyHICPPModule",
"clangTidyLinuxKernelModule",
"clangTidyLLVMModule",
"clangTidyLLVMLibcModule",
"clangTidyMiscModule",
"clangTidyModernizeModule",
"clangTidyObjCModule",
"clangTidyOpenMPModule",
"clangTidyPerformanceModule",
"clangTidyPortabilityModule",
"clangTidyReadabilityModule",
"clangTidyZirconModule",
"clangTooling",
"clangToolingCore",
"clangToolingInclusions",
"clangToolingInclusionsStdlib",
"clangToolingSyntax",
"clangToolingRefactoring",
"clangTransformer",
"clangCrossTU",
"clangAnalysisFlowSensitive",
"clangAnalysisFlowSensitiveModels",
"clangStaticAnalyzerCheckers",
"clangIncludeCleaner",
"llvm-headers",
"clang-headers",
"clang-tidy-headers",
"clang-resource-headers"
]
}

View File

@@ -40,52 +40,23 @@ def detect_platform() -> str:
raise RuntimeError(f"Unsupported platform: {plat}")
def detect_arch() -> str:
import platform
machine = platform.machine().lower()
if machine in ("x86_64", "amd64"):
return "x64"
if machine in ("aarch64", "arm64"):
return "arm64"
raise RuntimeError(f"Unsupported architecture: {machine}")
def pick_artifact(
manifest: list[dict],
version: str,
build_type: str,
is_lto: bool,
platform: str,
arch: str,
manifest: list[dict], version: str, build_type: str, is_lto: bool, platform: str
) -> dict:
base_version = version.split("+", 1)[0]
saw_missing_arch = False
for entry in manifest:
if entry.get("version") != version:
continue
if entry.get("platform") != platform.lower():
continue
entry_arch = entry.get("arch")
if entry_arch is None:
saw_missing_arch = True
continue
if entry_arch != arch:
continue
if entry.get("build_type") != build_type:
continue
if bool(entry.get("lto")) != is_lto:
continue
return entry
if saw_missing_arch:
raise RuntimeError(
f"Manifest contains entries without an 'arch' field for version={base_version}, "
f"platform={platform}. The manifest format changed to require explicit "
f"architectures; regenerate it via scripts/update-llvm-version.py."
)
raise RuntimeError(
f"No matching LLVM artifact in manifest for version={base_version}, platform={platform}, "
f"arch={arch}, build_type={build_type}, lto={is_lto}"
f"build_type={build_type}, lto={is_lto}"
)
@@ -293,14 +264,6 @@ def main() -> None:
parser.add_argument("--install-path")
parser.add_argument("--enable-lto", action="store_true")
parser.add_argument("--offline", action="store_true")
parser.add_argument(
"--target-platform",
help="Override platform for cross-compilation (e.g. macosx, linux, windows)",
)
parser.add_argument(
"--target-arch",
help="Override architecture for cross-compilation (e.g. x64, arm64)",
)
parser.add_argument("--output", required=True)
args = parser.parse_args()
@@ -312,11 +275,8 @@ def main() -> None:
)
token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
build_type = args.build_type
platform_name = args.target_platform if args.target_platform else detect_platform()
arch_name = args.target_arch if args.target_arch else detect_arch()
log(
f"Platform: {platform_name}, arch: {arch_name}, normalized build type: {build_type}"
)
platform_name = detect_platform()
log(f"Platform detected: {platform_name}, normalized build type: {build_type}")
manifest = read_manifest(Path(args.manifest))
binary_dir = Path(args.binary_dir).resolve()
@@ -344,12 +304,7 @@ def main() -> None:
if install_path is None:
needs_install = True
artifact = pick_artifact(
manifest,
args.version,
build_type,
args.enable_lto,
platform_name,
arch_name,
manifest, args.version, build_type, args.enable_lto, platform_name
)
log(f"Selected artifact: {artifact.get('filename')} for download")
filename = artifact["filename"]
@@ -362,12 +317,7 @@ def main() -> None:
install_path = install_root
elif needs_install:
artifact = pick_artifact(
manifest,
args.version,
build_type,
args.enable_lto,
platform_name,
arch_name,
manifest, args.version, build_type, args.enable_lto, platform_name
)
log(f"Selected artifact: {artifact.get('filename')} for download")
filename = artifact["filename"]

View File

@@ -1,162 +0,0 @@
#!/usr/bin/env python3
import argparse
import json
import re
import sys
from pathlib import Path
def copy_manifest(src: Path, dest: Path) -> None:
text = src.read_text(encoding="utf-8")
try:
data = json.loads(text)
except json.JSONDecodeError as err:
print(f"Error: {src} is not valid JSON: {err}", file=sys.stderr)
sys.exit(1)
if not isinstance(data, list) or len(data) == 0:
print(f"Error: {src} must be a non-empty JSON array", file=sys.stderr)
sys.exit(1)
dest.parent.mkdir(parents=True, exist_ok=True)
with dest.open("w", encoding="utf-8") as handle:
json.dump(data, handle, indent=2)
handle.write("\n")
print(f"Copied manifest: {src} -> {dest} ({len(data)} entries)")
def update_package_cmake(path: Path, version: str) -> None:
text = path.read_text(encoding="utf-8")
pattern = r'setup_llvm\("[^"]*"\)'
matches = re.findall(pattern, text)
if len(matches) == 0:
print(f"Error: no setup_llvm(...) call found in {path}", file=sys.stderr)
sys.exit(1)
if len(matches) > 1:
print(
f"Error: expected exactly 1 setup_llvm(...) call in {path}, "
f"found {len(matches)}",
file=sys.stderr,
)
sys.exit(1)
old_call = matches[0]
new_call = f'setup_llvm("{version}")'
if old_call == new_call:
print(f"Version in {path} is already {version}, no change needed")
return
updated = text.replace(old_call, new_call)
path.write_text(updated, encoding="utf-8")
print(f"Updated {path}: {old_call} -> {new_call}")
def check_package_cmake(path: Path) -> None:
"""Verify package.cmake has exactly one setup_llvm(...) call that the
update script can rewrite. Used by CI to catch drift before the next bump."""
text = path.read_text(encoding="utf-8")
matches = re.findall(r'setup_llvm\("[^"]*"\)', text)
if len(matches) == 0:
print(f"Error: no setup_llvm(...) call found in {path}", file=sys.stderr)
sys.exit(1)
if len(matches) > 1:
print(
f"Error: expected exactly 1 setup_llvm(...) call in {path}, "
f"found {len(matches)}: {matches}",
file=sys.stderr,
)
sys.exit(1)
print(f"OK: {path} has a single setup_llvm(...) call: {matches[0]}")
def check_manifest(path: Path) -> None:
"""Verify the manifest is a well-formed non-empty array with required fields."""
try:
data = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as err:
print(f"Error: {path} is not valid JSON: {err}", file=sys.stderr)
sys.exit(1)
if not isinstance(data, list) or len(data) == 0:
print(f"Error: {path} must be a non-empty JSON array", file=sys.stderr)
sys.exit(1)
required = ("version", "platform", "arch", "build_type", "filename", "sha256")
for idx, entry in enumerate(data):
missing = [k for k in required if k not in entry]
if missing:
print(
f"Error: {path} entry {idx} is missing fields: {missing}",
file=sys.stderr,
)
sys.exit(1)
print(f"OK: {path} has {len(data)} well-formed entries")
def main() -> None:
parser = argparse.ArgumentParser(
description="Update LLVM version references in the clice project."
)
parser.add_argument(
"--check",
action="store_true",
help="Validate existing state without modifying files (for CI drift checks)",
)
parser.add_argument(
"--version",
help="New LLVM version string (e.g. 21.2.0); required unless --check",
)
parser.add_argument(
"--manifest-src",
help="Path to the source llvm-manifest.json; required unless --check",
)
parser.add_argument(
"--manifest-dest",
required=True,
help="Path to destination manifest (e.g. config/llvm-manifest.json)",
)
parser.add_argument(
"--package-cmake",
required=True,
help="Path to cmake/package.cmake",
)
args = parser.parse_args()
manifest_dest = Path(args.manifest_dest)
package_cmake = Path(args.package_cmake)
if not package_cmake.is_file():
print(f"Error: package.cmake not found: {package_cmake}", file=sys.stderr)
sys.exit(1)
if args.check:
check_package_cmake(package_cmake)
check_manifest(manifest_dest)
print("Done (check mode).")
return
if not args.version or not args.manifest_src:
print(
"Error: --version and --manifest-src are required unless --check is set",
file=sys.stderr,
)
sys.exit(1)
manifest_src = Path(args.manifest_src)
if not manifest_src.is_file():
print(f"Error: manifest source not found: {manifest_src}", file=sys.stderr)
sys.exit(1)
copy_manifest(manifest_src, manifest_dest)
update_package_cmake(package_cmake, args.version)
print("Done.")
if __name__ == "__main__":
main()

View File

@@ -27,15 +27,6 @@ def parse_platform(name: str) -> str:
raise ValueError(f"Unable to determine platform from filename: {name}")
def parse_arch(name: str) -> str:
lowered = name.lower()
if lowered.startswith("aarch64-") or lowered.startswith("arm64-"):
return "arm64"
if lowered.startswith("x64-") or lowered.startswith("x86_64-"):
return "x64"
raise ValueError(f"Unable to determine arch from filename: {name}")
def parse_build_type(name: str) -> str:
lowered = name.lower()
if "debug" in lowered:
@@ -52,7 +43,6 @@ def build_metadata_entry(path: Path, version: str) -> dict:
"lto": "-lto" in filename.lower(),
"asan": "-asan" in filename.lower(),
"platform": parse_platform(filename),
"arch": parse_arch(filename),
"build_type": parse_build_type(filename),
}

View File

@@ -1,163 +0,0 @@
#!/usr/bin/env python3
"""
Validate the LLVM distribution component list against the actual LLVM source tree.
Scans the LLVM source for CMake library targets and compares them against
a components JSON file to detect stale or misspelled entries.
"""
import argparse
import difflib
import json
import re
import sys
from pathlib import Path
# CMake function calls that define library targets.
# The captured group uses [^\s)]+ to grab the target name without
# trailing parentheses or whitespace.
LLVM_LIB_PATTERNS = [
re.compile(r"add_llvm_component_library\(\s*([^\s)]+)"),
re.compile(r"add_llvm_library\(\s*([^\s)]+)"),
]
CLANG_LIB_PATTERNS = [
re.compile(r"add_clang_library\(\s*([^\s)]+)"),
]
# Header-only / custom install targets.
HEADER_PATTERNS = [
re.compile(r"add_llvm_install_targets\(\s*([^\s)]+)"),
re.compile(r"add_custom_target\(\s*([^\s)]+)"),
re.compile(r"add_library\(\s*([^\s)]+)"),
]
# Targets we recognise as header-only distribution components.
KNOWN_HEADER_TARGETS = {
"llvm-headers",
"clang-headers",
"clang-tidy-headers",
"clang-resource-headers",
}
def scan_targets(directory: Path, patterns: list[re.Pattern]) -> set[str]:
"""Recursively scan *directory* for CMakeLists.txt files and extract target names."""
targets: set[str] = set()
if not directory.is_dir():
return targets
for cmake_file in directory.rglob("CMakeLists.txt"):
text = cmake_file.read_text(errors="replace")
for pattern in patterns:
for match in pattern.finditer(text):
targets.add(match.group(1))
return targets
def scan_header_targets(llvm_src: Path) -> set[str]:
"""Scan for well-known header / custom-install targets across the tree."""
found: set[str] = set()
for cmake_file in llvm_src.rglob("CMakeLists.txt"):
text = cmake_file.read_text(errors="replace")
for pattern in HEADER_PATTERNS:
for match in pattern.finditer(text):
name = match.group(1)
if name in KNOWN_HEADER_TARGETS:
found.add(name)
return found
def collect_source_targets(llvm_src: Path) -> set[str]:
"""Return the full set of library / header targets found in the LLVM source tree."""
targets: set[str] = set()
targets |= scan_targets(llvm_src / "llvm" / "lib", LLVM_LIB_PATTERNS)
targets |= scan_targets(llvm_src / "clang" / "lib", CLANG_LIB_PATTERNS)
targets |= scan_targets(llvm_src / "clang-tools-extra", CLANG_LIB_PATTERNS)
targets |= scan_header_targets(llvm_src)
return targets
def load_components(path: Path) -> list[str]:
with path.open("r", encoding="utf-8") as handle:
data = json.load(handle)
if isinstance(data, dict):
data = data.get("components", [])
if not isinstance(data, list) or not data:
print(f"Error: no component list found in {path}", file=sys.stderr)
sys.exit(1)
return data
def main() -> None:
parser = argparse.ArgumentParser(
description="Validate LLVM distribution components against the source tree."
)
parser.add_argument(
"--llvm-src",
required=True,
help="Path to the llvm-project source root",
)
parser.add_argument(
"--components-file",
required=True,
help="Path to llvm-components.json",
)
args = parser.parse_args()
llvm_src = Path(args.llvm_src).expanduser().resolve()
components_file = Path(args.components_file).expanduser().resolve()
if not llvm_src.is_dir():
print(f"Error: LLVM source directory not found: {llvm_src}")
sys.exit(1)
if not (llvm_src / "llvm" / "CMakeLists.txt").exists():
print(f"Error: {llvm_src} does not look like an llvm-project root.")
sys.exit(1)
if not components_file.is_file():
print(f"Error: components file not found: {components_file}")
sys.exit(1)
components = load_components(components_file)
source_targets = collect_source_targets(llvm_src)
print(f"Found {len(source_targets)} targets in LLVM source tree")
print(f"Components file lists {len(components)} entries")
# Check for components that are missing from the source tree.
missing: list[tuple[str, list[str]]] = []
for name in components:
if name not in source_targets:
suggestions = difflib.get_close_matches(
name, source_targets, n=3, cutoff=0.6
)
missing.append((name, suggestions))
if missing:
print(f"\nError: {len(missing)} component(s) not found in the source tree:\n")
for name, suggestions in missing:
print(f" - {name}")
if suggestions:
print(f" Did you mean: {', '.join(suggestions)}?")
sys.exit(1)
# Warn about source targets not present in the component list.
component_set = set(components)
new_targets = sorted(source_targets - component_set - KNOWN_HEADER_TARGETS)
# Filter to targets that follow LLVM/Clang naming conventions to reduce noise.
noteworthy = [t for t in new_targets if t.startswith(("LLVM", "clang", "Clang"))]
if noteworthy:
print(
f"\nWarning: {len(noteworthy)} target(s) in source not listed in components:"
)
for name in noteworthy:
print(f" + {name}")
print("\nAll components validated successfully.")
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -4,21 +4,19 @@
#include <print>
#include <string>
#include "eventide/async/async.h"
#include "eventide/deco/deco.h"
#include "eventide/ipc/peer.h"
#include "eventide/ipc/recording_transport.h"
#include "eventide/ipc/transport.h"
#include "server/master_server.h"
#include "server/stateful_worker.h"
#include "server/stateless_worker.h"
#include "support/logging.h"
#include "kota/async/async.h"
#include "kota/deco/deco.h"
#include "kota/ipc/codec/json.h"
#include "kota/ipc/peer.h"
#include "kota/ipc/recording_transport.h"
#include "kota/ipc/transport.h"
namespace clice {
using kota::deco::decl::KVStyle;
using deco::decl::KVStyle;
struct Options {
DecoKV(style = KVStyle::JoinedOrSeparate,
@@ -74,8 +72,8 @@ int main(int argc, const char** argv) {
signal(SIGPIPE, SIG_IGN);
#endif
auto args = kota::deco::util::argvify(argc, argv);
auto result = kota::deco::cli::parse<clice::Options>(args);
auto args = deco::util::argvify(argc, argv);
auto result = deco::cli::parse<clice::Options>(args);
if(!result.has_value()) {
LOG_ERROR("{}", result.error().message);
@@ -85,7 +83,7 @@ int main(int argc, const char** argv) {
auto& opts = result->options;
if(opts.help.value_or(false)) {
kota::deco::cli::write_usage_for<clice::Options>(std::cout, "clice [OPTIONS]");
deco::cli::write_usage_for<clice::Options>(std::cout, "clice [OPTIONS]");
return 0;
}
@@ -134,22 +132,23 @@ int main(int argc, const char** argv) {
if(mode == "pipe") {
clice::logging::stderr_logger("master", clice::logging::options);
kota::event_loop loop;
namespace et = eventide;
et::event_loop loop;
auto transport = kota::ipc::StreamTransport::open_stdio(loop);
auto transport = et::ipc::StreamTransport::open_stdio(loop);
if(!transport) {
LOG_ERROR("failed to open stdio transport");
return 1;
}
std::unique_ptr<kota::ipc::Transport> final_transport = std::move(*transport);
std::unique_ptr<et::ipc::Transport> final_transport = std::move(*transport);
if(opts.record.has_value()) {
final_transport =
std::make_unique<kota::ipc::RecordingTransport>(std::move(final_transport),
*opts.record);
std::make_unique<et::ipc::RecordingTransport>(std::move(final_transport),
*opts.record);
}
kota::ipc::JsonPeer peer(loop, std::move(final_transport));
et::ipc::JsonPeer peer(loop, std::move(final_transport));
clice::MasterServer server(loop, peer, std::move(self_path));
server.register_handlers();
@@ -161,12 +160,13 @@ int main(int argc, const char** argv) {
if(mode == "socket") {
clice::logging::stderr_logger("master", clice::logging::options);
kota::event_loop loop;
namespace et = eventide;
et::event_loop loop;
auto host = opts.host.value_or("127.0.0.1");
auto port = opts.port.value_or(50051);
auto acceptor = kota::tcp::listen(host, port, {}, loop);
auto acceptor = et::tcp::listen(host, port, {}, loop);
if(!acceptor) {
LOG_ERROR("failed to listen on {}:{}", host, port);
return 1;
@@ -174,7 +174,7 @@ int main(int argc, const char** argv) {
LOG_INFO("Listening on {}:{} ...", host, port);
auto task = [&]() -> kota::task<> {
auto task = [&]() -> et::task<> {
auto client = co_await acceptor->accept();
if(!client.has_value()) {
LOG_ERROR("failed to accept connection");
@@ -184,13 +184,13 @@ int main(int argc, const char** argv) {
LOG_INFO("Client connected");
std::unique_ptr<kota::ipc::Transport> transport =
std::make_unique<kota::ipc::StreamTransport>(std::move(client.value()));
std::unique_ptr<et::ipc::Transport> transport =
std::make_unique<et::ipc::StreamTransport>(std::move(client.value()));
if(opts.record.has_value()) {
transport = std::make_unique<kota::ipc::RecordingTransport>(std::move(transport),
*opts.record);
transport = std::make_unique<et::ipc::RecordingTransport>(std::move(transport),
*opts.record);
}
kota::ipc::JsonPeer peer(loop, std::move(transport));
et::ipc::JsonPeer peer(loop, std::move(transport));
clice::MasterServer server(loop, peer, std::string(self_path));
server.register_handlers();

View File

@@ -5,10 +5,10 @@
#include <vector>
#include "command/argument_parser.h"
#include "eventide/reflection/enum.h"
#include "support/filesystem.h"
#include "support/logging.h"
#include "kota/meta/enum.h"
#include "llvm/ADT/ScopeExit.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/FileSystem.h"
@@ -363,7 +363,7 @@ std::vector<const char*> query_toolchain(const QueryParams& params) {
case CompilerFamily::Unknown: {
/// TODO: nvcc and intel compilers need further exploration.
LOG_ERROR("Fail to query driver, unknown supported driver kind: {}, driver is {}",
kota::meta::enum_name(family),
eventide::refl::enum_name(family),
driver);
std::vector<const char*> result;

View File

@@ -219,10 +219,9 @@ public:
auto CreateASTConsumer(clang::CompilerInstance& instance, llvm::StringRef file)
-> std::unique_ptr<clang::ASTConsumer> final {
auto consumer = WrapperFrontendAction::CreateASTConsumer(instance, file);
if(!consumer)
return nullptr;
return std::make_unique<ProxyASTConsumer>(std::move(consumer), unit);
return std::make_unique<ProxyASTConsumer>(
WrapperFrontendAction::CreateASTConsumer(instance, file),
unit);
}
/// Make this public.
@@ -242,7 +241,6 @@ CompilationStatus CompilationUnitRef::Self::run_clang(
std::unique_ptr diagnostic_consumer = self.create_diagnostic();
std::unique_ptr invocation = self.create_invocation(params, diagnostic_consumer.get());
if(!invocation) {
LOG_WARN("run_clang: invocation creation failed");
return CompilationStatus::SetupFail;
}
@@ -257,7 +255,6 @@ CompilationStatus CompilationUnitRef::Self::run_clang(
}
if(!instance.createTarget()) {
LOG_WARN("run_clang: target creation failed");
return CompilationStatus::SetupFail;
}
@@ -272,7 +269,6 @@ CompilationStatus CompilationUnitRef::Self::run_clang(
/// But if we fail to `BeginSourceFile` we don't need to call `EndSourceFile`. So just
/// reset it.
self.action.reset();
LOG_WARN("run_clang: BeginSourceFile failed");
return CompilationStatus::SetupFail;
}
@@ -306,8 +302,6 @@ CompilationStatus CompilationUnitRef::Self::run_clang(
/// in crash frequently. So forbidden it here and return as error.
if(!instance.getFrontendOpts().OutputFile.empty() &&
instance.getDiagnostics().hasErrorOccurred()) {
LOG_WARN("run_clang: errors during PCH/PCM generation, output={}",
instance.getFrontendOpts().OutputFile);
return CompilationStatus::FatalError;
}

View File

@@ -81,8 +81,7 @@ auto CompilationUnitRef::file_offset(clang::SourceLocation location) -> std::uin
}
auto CompilationUnitRef::file_path(clang::FileID fid) -> llvm::StringRef {
if(!fid.isValid())
return {};
assert(fid.isValid() && "Invalid fid");
if(auto it = self->path_cache.find(fid); it != self->path_cache.end()) {
return it->second;
}

View File

@@ -296,8 +296,7 @@ public:
llvm::StringRef overload_key,
llvm::StringRef signature = {},
llvm::StringRef return_type = {},
bool is_snippet = false,
bool is_deprecated = false) {
bool is_snippet = false) {
if(label.empty()) {
return;
}
@@ -328,9 +327,6 @@ public:
}
item.label_details = std::move(details);
}
if(is_deprecated) {
item.tags = std::vector{protocol::CompletionItemTag::Deprecated};
}
overloads.push_back({
.item = std::move(item),
.score = *score,
@@ -359,9 +355,6 @@ public:
}
item.label_details = std::move(details);
}
if(is_deprecated) {
item.tags = std::vector{protocol::CompletionItemTag::Deprecated};
}
collected.push_back(std::move(item));
};
@@ -438,15 +431,13 @@ public:
bool has_snippet = !snippet.empty();
auto insert = has_snippet ? llvm::StringRef(snippet) : llvm::StringRef(label);
bool deprecated = candidate.Availability == CXAvailability_Deprecated;
try_add(label,
kind,
insert,
qualified_name.str(),
signature,
return_type,
has_snippet,
deprecated);
has_snippet);
break;
}
}

View File

@@ -2,15 +2,14 @@
#include <string>
#include <vector>
#include "eventide/ipc/lsp/uri.h"
#include "feature/feature.h"
#include "kota/ipc/lsp/uri.h"
namespace clice::feature {
namespace {
namespace lsp = kota::ipc::lsp;
namespace lsp = eventide::ipc::lsp;
auto to_uri(llvm::StringRef file) -> std::string {
const auto file_view = std::string_view(file.data(), file.size());

View File

@@ -7,6 +7,15 @@
namespace clice::feature {
namespace {
bool is_directive_keyword(llvm::StringRef word) {
return word == "include" || word == "include_next" || word == "import" || word == "embed" ||
word == "__has_include" || word == "__has_include_next" || word == "__has_embed";
}
} // namespace
auto document_links(CompilationUnitRef unit, PositionEncoding encoding)
-> std::vector<protocol::DocumentLink> {
std::vector<protocol::DocumentLink> links;
@@ -20,13 +29,64 @@ auto document_links(CompilationUnitRef unit, PositionEncoding encoding)
auto content = unit.interested_content();
PositionMapper converter(content, encoding);
auto& directives = directives_it->second;
auto* lang_opts = &unit.lang_options();
// Find the filename argument of a preprocessor directive starting from `offset`.
// Creates a Lexer from the line start so that # at start-of-line is detected,
// which enables header_name mode for #include and #embed automatically.
// For __has_include/__has_embed, manually enables header_name mode after (.
auto find_argument_range = [&](std::uint32_t offset) -> std::optional<LocalSourceRange> {
std::uint32_t line_start = 0;
if(offset > 0) {
if(auto nl = content.rfind('\n', offset - 1); nl != llvm::StringRef::npos)
line_start = static_cast<std::uint32_t>(nl + 1);
}
auto line = content.substr(line_start);
Lexer lexer(line);
bool after_has_keyword = false;
while(true) {
auto tok = lexer.advance();
if(tok.is_eof() || tok.is_eod())
break;
auto abs_begin = line_start + tok.range.begin;
auto abs_end = line_start + tok.range.end;
// Detect __has_include/__has_embed to enable header_name mode after (.
if(tok.is_identifier()) {
auto text = tok.text(line);
if(text == "__has_include" || text == "__has_include_next" ||
text == "__has_embed") {
after_has_keyword = true;
continue;
}
}
if(tok.kind == clang::tok::l_paren && after_has_keyword) {
after_has_keyword = false;
lexer.set_header_name_mode();
continue;
}
// Only return tokens at or after the directive's starting offset.
if(abs_begin < offset)
continue;
if(tok.is_header_name() || tok.kind == clang::tok::string_literal)
return LocalSourceRange(abs_begin, abs_end);
if(tok.is_identifier() && !is_directive_keyword(tok.text(line)))
return LocalSourceRange(abs_begin, abs_end);
}
return std::nullopt;
};
auto add_link = [&](clang::SourceLocation loc, llvm::StringRef target) {
auto [fid, offset] = unit.decompose_location(loc);
if(fid != interested || offset >= content.size())
return;
auto range = find_directive_argument(content, offset, lang_opts);
auto range = find_argument_range(offset);
if(!range)
return;
protocol::DocumentLink link{.range = to_range(converter, *range)};

View File

@@ -7,9 +7,8 @@
#include "compile/compilation.h"
#include "compile/compilation_unit.h"
#include "kota/ipc/lsp/position.h"
#include "kota/ipc/lsp/protocol.h"
#include "eventide/ipc/lsp/position.h"
#include "eventide/ipc/lsp/protocol.h"
namespace clang {
@@ -19,11 +18,11 @@ class NamedDecl;
namespace clice::feature {
namespace protocol = kota::ipc::protocol;
namespace protocol = eventide::ipc::protocol;
using kota::ipc::lsp::PositionEncoding;
using kota::ipc::lsp::PositionMapper;
using kota::ipc::lsp::parse_position_encoding;
using eventide::ipc::lsp::PositionEncoding;
using eventide::ipc::lsp::PositionMapper;
using eventide::ipc::lsp::parse_position_encoding;
inline auto to_range(const PositionMapper& converter, LocalSourceRange range) -> protocol::Range {
return protocol::Range{

View File

@@ -12,7 +12,6 @@
#include "clang/AST/Attr.h"
#include "clang/Basic/IdentifierTable.h"
#include "clang/Basic/Module.h"
namespace clice::feature {
@@ -169,7 +168,6 @@ public:
auto collect() -> std::vector<RawToken> {
highlight_lexical(unit.interested_file());
run();
highlight_modules();
merge_tokens();
return std::move(tokens);
}
@@ -293,58 +291,6 @@ private:
});
}
void highlight_modules() {
auto interested = unit.interested_file();
auto directives_it = unit.directives().find(interested);
if(directives_it != unit.directives().end()) {
for(const auto& import: directives_it->second.imports) {
add_token(import.location, SymbolKind::Keyword, 0);
for(auto loc: import.name_locations) {
add_token(loc, SymbolKind::Module, 0);
}
}
}
auto* mod = unit.context().getCurrentNamedModule();
if(!mod) {
return;
}
auto def_loc = mod->DefinitionLoc;
if(!def_loc.isValid() || !def_loc.isFileID()) {
return;
}
auto [fid, offset] = unit.decompose_location(def_loc);
if(fid != interested) {
return;
}
auto content = unit.file_content(fid);
auto& lang_opts = unit.lang_options();
Lexer lexer(content.substr(offset), false, &lang_opts);
auto module_token = lexer.advance();
if(module_token.is_identifier()) {
auto range = LocalSourceRange(offset + module_token.range.begin,
offset + module_token.range.end);
tokens.push_back({.range = range, .kind = SymbolKind::Keyword, .modifiers = 0});
}
// Scan for identifiers (module name parts) until semicolon/eof.
while(true) {
auto token = lexer.advance();
if(token.is_eof() || token.kind == clang::tok::semi) {
break;
}
if(token.is_identifier()) {
auto range = LocalSourceRange(offset + token.range.begin, offset + token.range.end);
tokens.push_back({.range = range, .kind = SymbolKind::Module, .modifiers = 0});
}
}
}
void highlight_lexical(clang::FileID fid) {
auto content = unit.file_content(fid);
auto& lang_opts = unit.lang_options();
@@ -399,17 +345,10 @@ private:
}
static void resolve_conflict(RawToken& last, const RawToken& current) {
(void)current;
if(last.kind == SymbolKind::Conflict) {
return;
}
// Directive is a low-priority lexical kind; semantic tokens override it.
if(last.kind == SymbolKind::Directive) {
last = current;
return;
}
if(current.kind == SymbolKind::Directive) {
return;
}
last.kind = SymbolKind::Conflict;
}

View File

@@ -308,10 +308,6 @@ const clang::NamedDecl* decl_of_impl(const void* T) {
}
auto decl_of(clang::QualType type) -> const clang::NamedDecl* {
if(type.isNull()) {
return nullptr;
}
// Strip type-sugar that wraps the underlying type without adding a decl
// (e.g. ElaboratedType for "struct Foo" vs plain "Foo").
if(auto ET = type->getAs<clang::ElaboratedType>()) {

View File

@@ -131,6 +131,33 @@ public:
}
}
}
// if(auto module = unit.context().getCurrentNamedModule()) {
// auto keyword = module->DefinitionLoc;
// auto begin = TB.spelledTokenContaining(keyword);
// // assert(begin->kind() == clang::tok::identifier && begin->text(SM) == "module" &&
// // "Invalid module declaration");
//
// begin += 1;
// auto end = TB.spelledTokens(unit.file_id(keyword)).end();
//
// for(auto iter = begin; iter != end; ++iter) {
// if(iter->kind() == clang::tok::identifier) {
// if(auto next = iter + 1; next != end && (next->kind() == clang::tok::period ||
// next->kind() == clang::tok::colon)) {
// iter += 1;
// continue;
// }
//
// end = iter + 1;
// break;
// }
//
// std::unreachable();
// }
//
// handleModuleOccurrence(keyword, llvm::ArrayRef<clang::syntax::Token>(begin, end));
//}
}
public:

View File

@@ -33,19 +33,19 @@ void CompileGraph::ensure_resolved(std::uint32_t path_id) {
}
}
kota::task<bool> CompileGraph::compile_deps(std::uint32_t path_id) {
et::task<bool> CompileGraph::compile_deps(std::uint32_t path_id) {
llvm::DenseSet<std::uint32_t> ancestors;
co_return co_await compile_impl(path_id, ancestors, false);
}
kota::task<bool> CompileGraph::compile(std::uint32_t path_id) {
et::task<bool> CompileGraph::compile(std::uint32_t path_id) {
llvm::DenseSet<std::uint32_t> ancestors;
co_return co_await compile_impl(path_id, ancestors);
}
kota::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
llvm::DenseSet<std::uint32_t> ancestors,
bool dispatch_self) {
et::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
llvm::DenseSet<std::uint32_t> ancestors,
bool dispatch_self) {
ensure_resolved(path_id);
// Cycle detection: if this unit is already in the compile chain, bail out.
@@ -63,12 +63,12 @@ kota::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
co_return true;
}
std::vector<kota::task<bool>> dep_tasks;
std::vector<et::task<bool>> dep_tasks;
dep_tasks.reserve(deps.size());
for(auto dep_id: deps) {
dep_tasks.push_back(compile_impl(dep_id, ancestors));
}
auto results = co_await kota::when_all(std::move(dep_tasks));
auto results = co_await et::when_all(std::move(dep_tasks));
for(auto ok: results) {
if(!ok) {
co_return false;
@@ -96,7 +96,7 @@ kota::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
// Begin compilation. The finish lambda ensures compiling/completion state
// is always cleaned up, regardless of how the function exits.
it->second.compiling = true;
it->second.completion = std::make_unique<kota::event>();
it->second.completion = std::make_unique<et::event>();
auto finish = [&, path_id] {
auto& u = units.find(path_id)->second;
@@ -113,17 +113,17 @@ kota::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
// Deadlocks from cross-branch cycles (e.g. 1->{2,3}, 2->3, 3->2) are
// prevented by has_wait_cycle() checking before completion.wait().
if(!deps.empty()) {
std::vector<kota::task<bool, void, kota::cancellation>> dep_tasks;
std::vector<et::task<bool, void, et::cancellation>> dep_tasks;
dep_tasks.reserve(deps.size());
for(auto dep_id: deps) {
dep_tasks.push_back(kota::with_token(compile_impl(dep_id, ancestors), token));
dep_tasks.push_back(et::with_token(compile_impl(dep_id, ancestors), token));
}
auto results = co_await kota::when_all(std::move(dep_tasks));
auto results = co_await et::when_all(std::move(dep_tasks));
if(results.is_cancelled()) {
finish();
co_await kota::cancel();
co_await et::cancel();
}
for(auto ok: *results) {
@@ -135,11 +135,11 @@ kota::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
}
// Dispatch the actual compilation, cancellable via the pre-captured token.
auto result = co_await kota::with_token(dispatch(path_id), token);
auto result = co_await et::with_token(dispatch(path_id), token);
if(!result.has_value()) {
finish();
co_await kota::cancel();
co_await et::cancel();
}
if(!*result) {
@@ -199,7 +199,7 @@ llvm::SmallVector<std::uint32_t> CompileGraph::update(std::uint32_t path_id) {
// Cancel in-flight compilation if running.
if(unit.compiling) {
unit.source->cancel();
unit.source = std::make_unique<kota::cancellation_source>();
unit.source = std::make_unique<et::cancellation_source>();
}
unit.dirty = true;
unit.generation++;
@@ -247,7 +247,7 @@ bool CompileGraph::has_wait_cycle(std::uint32_t target,
void CompileGraph::cancel_all() {
for(auto& [_, unit]: units) {
unit.source->cancel();
unit.source = std::make_unique<kota::cancellation_source>();
unit.source = std::make_unique<et::cancellation_source>();
}
}

View File

@@ -4,13 +4,16 @@
#include <functional>
#include <memory>
#include "kota/async/async.h"
#include "eventide/async/async.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/DenseSet.h"
#include "llvm/ADT/SmallVector.h"
namespace clice {
namespace et = eventide;
struct CompileUnit {
std::uint32_t path_id = 0;
@@ -30,15 +33,14 @@ struct CompileUnit {
/// stale completions without ABA risk from raw-pointer comparison.
std::uint64_t generation = 0;
std::unique_ptr<kota::cancellation_source> source =
std::make_unique<kota::cancellation_source>();
std::unique_ptr<kota::event> completion;
std::unique_ptr<et::cancellation_source> source = std::make_unique<et::cancellation_source>();
std::unique_ptr<et::event> completion;
};
class CompileGraph {
public:
/// Performs the actual compilation (e.g. produce PCM file).
using dispatch_fn = std::function<kota::task<bool>(std::uint32_t path_id)>;
using dispatch_fn = std::function<et::task<bool>(std::uint32_t path_id)>;
/// Returns the dependency path_ids for a given path_id (called lazily on first compile).
using resolve_fn = std::function<llvm::SmallVector<std::uint32_t>(std::uint32_t path_id)>;
@@ -46,11 +48,11 @@ public:
CompileGraph(dispatch_fn dispatch, resolve_fn resolve);
/// Compile a unit and all its transitive dependencies.
kota::task<bool> compile(std::uint32_t path_id);
et::task<bool> compile(std::uint32_t path_id);
/// Compile all transitive module dependencies of path_id, but NOT path_id itself.
/// Used for non-module files (plain .cpp) that import modules.
kota::task<bool> compile_deps(std::uint32_t path_id);
et::task<bool> compile_deps(std::uint32_t path_id);
/// Mark path_id and all transitive dependents as dirty,
/// cancelling any in-progress compilations.
@@ -68,9 +70,9 @@ private:
void ensure_resolved(std::uint32_t path_id);
/// Internal compile with ancestor tracking for cycle detection.
kota::task<bool> compile_impl(std::uint32_t path_id,
llvm::DenseSet<std::uint32_t> ancestors,
bool dispatch_self = true);
et::task<bool> compile_impl(std::uint32_t path_id,
llvm::DenseSet<std::uint32_t> ancestors,
bool dispatch_self = true);
/// Check if waiting on `target` would deadlock given our `ancestors` chain.
/// Walks the dependency graph through compiling units to see if any dep

View File

@@ -5,6 +5,9 @@
#include <string>
#include "command/search_config.h"
#include "eventide/ipc/lsp/position.h"
#include "eventide/ipc/lsp/uri.h"
#include "eventide/serde/json/json.h"
#include "index/tu_index.h"
#include "server/protocol.h"
#include "support/filesystem.h"
@@ -12,9 +15,6 @@
#include "syntax/include_resolver.h"
#include "syntax/scan.h"
#include "kota/codec/json/json.h"
#include "kota/ipc/lsp/position.h"
#include "kota/ipc/lsp/uri.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Path.h"
@@ -22,13 +22,13 @@
namespace clice {
namespace lsp = kota::ipc::lsp;
using serde_raw = kota::codec::RawValue;
namespace lsp = eventide::ipc::lsp;
using serde_raw = et::serde::RawValue;
/// Detect whether the cursor is inside a preamble directive (include/import).
Compiler::Compiler(kota::event_loop& loop,
kota::ipc::JsonPeer& peer,
Compiler::Compiler(et::event_loop& loop,
et::ipc::JsonPeer& peer,
Workspace& workspace,
WorkerPool& pool,
llvm::DenseMap<std::uint32_t, Session>& sessions) :
@@ -47,13 +47,8 @@ void Compiler::init_compile_graph() {
// Lazy dependency resolver: scans a module file on demand to discover imports.
auto resolve = [this](std::uint32_t path_id) -> llvm::SmallVector<std::uint32_t> {
auto file_path = workspace.path_pool.resolve(path_id);
std::vector<std::string> rule_append, rule_remove;
workspace.config.match_rules(file_path, rule_append, rule_remove);
auto results = workspace.cdb.lookup(file_path,
{.query_toolchain = true,
.suppress_logging = true,
.remove = rule_remove,
.append = rule_append});
auto results =
workspace.cdb.lookup(file_path, {.query_toolchain = true, .suppress_logging = true});
if(results.empty())
return {};
@@ -80,7 +75,7 @@ void Compiler::init_compile_graph() {
};
// Dispatch: sends BuildPCM request to a stateless worker.
auto dispatch = [this](std::uint32_t path_id) -> kota::task<bool> {
auto dispatch = [this](std::uint32_t path_id) -> et::task<bool> {
auto mod_it = workspace.path_to_module.find(path_id);
if(mod_it == workspace.path_to_module.end())
co_return false;
@@ -102,8 +97,7 @@ void Compiler::init_compile_graph() {
}
auto args_hash = llvm::xxh3_64bits(llvm::StringRef(hash_input));
auto pcm_filename = std::format("{}-{:016x}.pcm", safe_module_name, args_hash);
auto pcm_path =
path::join(workspace.config.project.cache_dir, "cache", "pcm", pcm_filename);
auto pcm_path = path::join(workspace.config.cache_dir, "cache", "pcm", pcm_filename);
// Check if cached PCM is still valid.
if(auto pcm_it = workspace.pcm_cache.find(path_id); pcm_it != workspace.pcm_cache.end()) {
@@ -122,11 +116,9 @@ void Compiler::init_compile_graph() {
auto result = co_await pool.send_stateless(bp);
if(!result.has_value() || !result.value().success) {
auto error_msg = result.has_value() ? result.value().error : result.error().message;
LOG_WARN("BuildPCM failed for module {}: {}", mod_it->second, error_msg);
peer.send_notification(protocol::LogMessageParams{
protocol::MessageType::Warning,
std::format("PCM build failed for module {}: {}", mod_it->second, error_msg)});
LOG_WARN("BuildPCM failed for module {}: {}",
mod_it->second,
result.has_value() ? result.value().error : result.error().message);
co_return false;
}
@@ -164,19 +156,11 @@ bool Compiler::fill_compile_args(llvm::StringRef path,
}
// 2. Normal CDB lookup for the file itself.
// Apply rules from config (append/remove flags based on file patterns).
std::vector<std::string> rule_append, rule_remove;
workspace.config.match_rules(path, rule_append, rule_remove);
CommandOptions opts{.query_toolchain = true, .remove = rule_remove, .append = rule_append};
auto results = workspace.cdb.lookup(path, opts);
auto results = workspace.cdb.lookup(path, {.query_toolchain = true});
if(!results.empty()) {
auto& cmd = results.front();
directory = cmd.resolved.directory.str();
arguments = cmd.to_string_argv();
LOG_DEBUG("fill_compile_args: CDB match for {} (dir={}, {} args)",
path,
directory,
arguments.size());
return true;
}
@@ -221,13 +205,7 @@ bool Compiler::fill_header_context_args(llvm::StringRef path,
}
auto host_path = workspace.path_pool.resolve(ctx_ptr->host_path_id);
// Apply rules matching the HEADER path (what the user is editing) on top of
// the host's command — rules are expected to apply uniformly to every file.
std::vector<std::string> rule_append, rule_remove;
workspace.config.match_rules(path, rule_append, rule_remove);
auto host_results = workspace.cdb.lookup(
host_path,
{.query_toolchain = true, .remove = rule_remove, .append = rule_append});
auto host_results = workspace.cdb.lookup(host_path, {.query_toolchain = true});
if(host_results.empty()) {
LOG_WARN("fill_header_context_args: host {} has no CDB entry", host_path);
return false;
@@ -377,7 +355,7 @@ std::optional<HeaderFileContext> Compiler::resolve_header_context(std::uint32_t
// Hash the preamble and write to cache directory.
auto preamble_hash = llvm::xxh3_64bits(llvm::StringRef(preamble));
auto preamble_filename = std::format("{:016x}.h", preamble_hash);
auto preamble_dir = path::join(workspace.config.project.cache_dir, "header_context");
auto preamble_dir = path::join(workspace.config.cache_dir, "header_context");
auto preamble_path = path::join(preamble_dir, preamble_filename);
if(!llvm::sys::fs::exists(preamble_path)) {
@@ -415,10 +393,10 @@ std::string uri_to_path(const std::string& uri) {
void Compiler::publish_diagnostics(const std::string& uri,
int version,
const kota::codec::RawValue& diagnostics_json) {
const et::serde::RawValue& diagnostics_json) {
std::vector<protocol::Diagnostic> diagnostics;
if(!diagnostics_json.empty()) {
auto status = kota::codec::json::from_json(diagnostics_json.data, diagnostics);
auto status = et::serde::json::from_json(diagnostics_json.data, diagnostics);
if(!status) {
LOG_WARN("Failed to deserialize diagnostics JSON for {}", uri);
}
@@ -437,9 +415,9 @@ void Compiler::clear_diagnostics(const std::string& uri) {
peer.send_notification(params);
}
kota::task<bool> Compiler::ensure_pch(Session& session,
const std::string& directory,
const std::vector<std::string>& arguments) {
et::task<bool> Compiler::ensure_pch(Session& session,
const std::string& directory,
const std::vector<std::string>& arguments) {
auto path_id = session.path_id;
auto path = workspace.path_pool.resolve(path_id);
auto& text = session.text;
@@ -460,7 +438,7 @@ kota::task<bool> Compiler::ensure_pch(Session& session,
auto preamble_hash = llvm::xxh3_64bits(preamble_text);
// Deterministic content-addressed PCH path.
auto pch_path = path::join(workspace.config.project.cache_dir,
auto pch_path = path::join(workspace.config.cache_dir,
"cache",
"pch",
std::format("{:016x}.pch", preamble_hash));
@@ -493,25 +471,9 @@ kota::task<bool> Compiler::ensure_pch(Session& session,
}
// Register in-flight build so concurrent requests wait on us.
auto completion = std::make_shared<kota::event>();
auto completion = std::make_shared<et::event>();
workspace.pch_cache[path_id].building = completion;
if(workspace.config.project.cache_dir.empty()) {
LOG_WARN("PCH build skipped: cache_dir is not configured");
workspace.pch_cache[path_id].building.reset();
completion->set();
co_return false;
}
// Ensure the PCH cache directory exists.
auto pch_dir = path::join(workspace.config.project.cache_dir, "cache", "pch");
if(auto ec = llvm::sys::fs::create_directories(pch_dir)) {
LOG_WARN("Cannot create PCH cache dir {}: {}", pch_dir, ec.message());
workspace.pch_cache[path_id].building.reset();
completion->set();
co_return false;
}
// Build a new PCH via stateless worker.
worker::BuildParams bp;
bp.kind = worker::BuildKind::BuildPCH;
@@ -527,11 +489,9 @@ kota::task<bool> Compiler::ensure_pch(Session& session,
auto result = co_await pool.send_stateless(bp);
if(!result.has_value() || !result.value().success) {
auto error_msg = result.has_value() ? result.value().error : result.error().message;
LOG_WARN("PCH build failed for {}: {}", path, error_msg);
peer.send_notification(protocol::LogMessageParams{
protocol::MessageType::Warning,
std::format("PCH build failed for {}: {}", path, error_msg)});
LOG_WARN("PCH build failed for {}: {}",
path,
result.has_value() ? result.value().error : result.error().message);
workspace.pch_cache[path_id].building.reset();
completion->set();
co_return false;
@@ -559,11 +519,11 @@ kota::task<bool> Compiler::ensure_pch(Session& session,
/// Compile module dependencies, build/reuse PCH, and fill PCM paths.
/// Shared preparation step used by both ensure_compiled() (stateful path)
/// and forward_stateless() (completion/signatureHelp path).
kota::task<bool> Compiler::ensure_deps(Session& session,
const std::string& directory,
const std::vector<std::string>& arguments,
std::pair<std::string, uint32_t>& pch,
std::unordered_map<std::string, std::string>& pcms) {
et::task<bool> Compiler::ensure_deps(Session& session,
const std::string& directory,
const std::vector<std::string>& arguments,
std::pair<std::string, uint32_t>& pch,
std::unordered_map<std::string, std::string>& pcms) {
auto path_id = session.path_id;
// Compile C++20 module dependencies (PCMs).
@@ -660,7 +620,7 @@ void Compiler::record_deps(Session& session, llvm::ArrayRef<std::string> deps) {
/// task via loop.schedule(); subsequent ones wait on the shared event.
/// The detached task cannot be cancelled by LSP $/cancelRequest, preventing
/// the race where cancellation wakes all waiters and they all start compiles.
kota::task<bool> Compiler::ensure_compiled(Session& session) {
et::task<bool> Compiler::ensure_compiled(Session& session) {
auto path_id = session.path_id;
LOG_DEBUG("ensure_compiled: path_id={} version={} gen={} ast_dirty={}",
@@ -703,7 +663,7 @@ kota::task<bool> Compiler::ensure_compiled(Session& session) {
// from the sessions map after co_await (DenseMap may invalidate pointers).
loop.schedule([](Compiler* self,
std::uint32_t pid,
std::shared_ptr<Session::PendingCompile> pc) -> kota::task<> {
std::shared_ptr<Session::PendingCompile> pc) -> et::task<> {
// Re-lookup session from the sessions map (pointer may have been
// invalidated by DenseMap growth during co_await).
auto find_session = [&]() -> Session* {
@@ -738,10 +698,6 @@ kota::task<bool> Compiler::ensure_compiled(Session& session) {
params.version = sess->version;
params.text = sess->text;
if(!self->fill_compile_args(file_path, params.directory, params.arguments, sess)) {
LOG_WARN("ensure_compiled: no compile args for {}", uri_str);
self->peer.send_notification(protocol::LogMessageParams{
protocol::MessageType::Warning,
std::format("No compile arguments available for {}", file_path)});
finish_compile();
co_return;
}
@@ -749,9 +705,6 @@ kota::task<bool> Compiler::ensure_compiled(Session& session) {
if(!co_await self
->ensure_deps(*sess, params.directory, params.arguments, params.pch, params.pcms)) {
LOG_WARN("Dependency preparation failed for {}, skipping compile", uri_str);
self->peer.send_notification(protocol::LogMessageParams{
protocol::MessageType::Warning,
std::format("Dependency preparation failed for {}", file_path)});
finish_compile();
co_return;
}
@@ -783,9 +736,6 @@ kota::task<bool> Compiler::ensure_compiled(Session& session) {
if(!result.has_value()) {
LOG_WARN("Compile failed for {}: {}", uri_str, result.error().message);
self->peer.send_notification(protocol::LogMessageParams{
protocol::MessageType::Error,
std::format("Compilation failed for {}: {}", file_path, result.error().message)});
self->clear_diagnostics(uri_str);
finish_compile();
co_return;
@@ -834,17 +784,11 @@ Compiler::RawResult Compiler::forward_query(worker::QueryKind kind,
auto text = session.text;
if(!co_await ensure_compiled(session)) {
LOG_WARN("forward_query: compilation failed for {}", path);
co_await kota::fail("Compilation failed");
co_return serde_raw{"null"};
}
auto sit = sessions.find(path_id);
if(sit == sessions.end()) {
LOG_WARN("forward_query: session lost after compile for {}", path);
co_await kota::fail("Document was closed during compilation");
}
if(sit->second.ast_dirty) {
LOG_DEBUG("forward_query: still dirty after compile for {} (concurrent edit)", path);
if(sit == sessions.end() || sit->second.ast_dirty) {
co_return serde_raw{"null"};
}
@@ -856,13 +800,8 @@ Compiler::RawResult Compiler::forward_query(worker::QueryKind kind,
if(position) {
auto offset = mapper.to_offset(*position);
if(!offset) {
LOG_WARN("forward_query: invalid position {}:{} for {}",
position->line,
position->character,
path);
co_await kota::fail("Invalid position: failed to convert to byte offset");
}
if(!offset)
co_return serde_raw{"null"};
wp.offset = *offset;
}
@@ -876,8 +815,7 @@ Compiler::RawResult Compiler::forward_query(worker::QueryKind kind,
auto result = co_await pool.send_stateful(path_id, wp);
if(!result.has_value()) {
LOG_WARN("forward_query: worker failed for {}: {}", path, result.error().message);
co_await kota::fail(result.error().message);
co_return serde_raw{};
}
co_return std::move(result.value());
}
@@ -896,36 +834,27 @@ Compiler::RawResult Compiler::forward_build(worker::BuildKind kind,
wp.version = session.version;
wp.text = session.text;
if(!fill_compile_args(path, wp.directory, wp.arguments, &session)) {
LOG_WARN("forward_build: compile args not available for {}", path);
co_await kota::fail("Compile arguments not available");
co_return serde_raw{};
}
if(!co_await ensure_deps(session, wp.directory, wp.arguments, wp.pch, wp.pcms)) {
LOG_WARN("forward_build: dependency preparation failed for {}", path);
co_await kota::fail("Dependency preparation failed");
co_return serde_raw{};
}
// After co_await, verify session still exists.
if(sessions.find(path_id) == sessions.end()) {
LOG_WARN("forward_build: session lost after co_await for {}", path);
co_await kota::fail("Document was closed during compilation");
co_return serde_raw{};
}
lsp::PositionMapper mapper(wp.text, lsp::PositionEncoding::UTF16);
auto offset = mapper.to_offset(position);
if(!offset) {
LOG_WARN("forward_build: invalid position {}:{} for {}",
position.line,
position.character,
path);
co_await kota::fail("Invalid position: failed to convert to byte offset");
}
if(!offset)
co_return serde_raw{"null"};
wp.offset = *offset;
auto result = co_await pool.send_stateless(wp);
if(!result.has_value()) {
LOG_WARN("forward_build: worker failed for {}: {}", path, result.error().message);
co_await kota::fail(result.error().message);
co_return serde_raw{};
}
co_return std::move(result.value().result_json);
}
@@ -943,10 +872,8 @@ Compiler::RawResult Compiler::handle_completion(const protocol::Position& positi
pctx.kind == CompletionContext::IncludeAngled) {
std::string directory;
std::vector<std::string> arguments;
if(!fill_compile_args(path, directory, arguments)) {
LOG_WARN("handle_completion: compile args not available for {}", path);
co_await kota::fail("Compile arguments not available for include completion");
}
if(!fill_compile_args(path, directory, arguments))
co_return serde_raw{"[]"};
std::vector<const char*> args_ptrs;
args_ptrs.reserve(arguments.size());
@@ -967,7 +894,7 @@ Compiler::RawResult Compiler::handle_completion(const protocol::Position& positi
item.kind = protocol::CompletionItemKind::File;
items.push_back(std::move(item));
}
auto json = kota::codec::json::to_json<kota::ipc::lsp_config>(items);
auto json = et::serde::json::to_json<et::ipc::lsp_config>(items);
co_return serde_raw{json ? std::move(*json) : "[]"};
}
if(pctx.kind == CompletionContext::Import) {
@@ -982,7 +909,7 @@ Compiler::RawResult Compiler::handle_completion(const protocol::Position& positi
item.insert_text = name + ";";
items.push_back(std::move(item));
}
auto json = kota::codec::json::to_json<kota::ipc::lsp_config>(items);
auto json = et::serde::json::to_json<et::ipc::lsp_config>(items);
co_return serde_raw{json ? std::move(*json) : "[]"};
}
}

View File

@@ -8,16 +8,15 @@
#include <vector>
#include "command/command.h"
#include "eventide/async/async.h"
#include "eventide/ipc/lsp/protocol.h"
#include "eventide/ipc/peer.h"
#include "eventide/serde/serde/raw_value.h"
#include "server/session.h"
#include "server/worker_pool.h"
#include "server/workspace.h"
#include "syntax/completion.h"
#include "kota/async/async.h"
#include "kota/codec/json/json.h"
#include "kota/ipc/codec/json.h"
#include "kota/ipc/lsp/protocol.h"
#include "kota/ipc/peer.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/SmallVector.h"
@@ -25,7 +24,8 @@
namespace clice {
namespace protocol = kota::ipc::protocol;
namespace et = eventide;
namespace protocol = et::ipc::protocol;
/// Convert a file:// URI to a local file path.
std::string uri_to_path(const std::string& uri);
@@ -49,8 +49,8 @@ std::string uri_to_path(const std::string& uri);
/// - Background indexing scheduling — handled by Indexer
class Compiler {
public:
Compiler(kota::event_loop& loop,
kota::ipc::JsonPeer& peer,
Compiler(et::event_loop& loop,
et::ipc::JsonPeer& peer,
Workspace& workspace,
WorkerPool& pool,
llvm::DenseMap<std::uint32_t, Session>& sessions);
@@ -67,9 +67,9 @@ public:
/// Compile an open file's AST if dirty. On success, updates session's
/// file_index, pch_ref, ast_deps, and publishes diagnostics.
kota::task<bool> ensure_compiled(Session& session);
et::task<bool> ensure_compiled(Session& session);
using RawResult = kota::task<kota::codec::RawValue, kota::ipc::Error>;
using RawResult = et::task<et::serde::RawValue, et::ipc::Error>;
/// Forward a query to the stateful worker that holds this file's AST.
/// Ensures compilation first. For position-sensitive queries (hover,
@@ -97,22 +97,20 @@ public:
std::function<void()> on_indexing_needed;
private:
kota::task<bool> ensure_deps(Session& session,
const std::string& directory,
const std::vector<std::string>& arguments,
std::pair<std::string, uint32_t>& pch,
std::unordered_map<std::string, std::string>& pcms);
et::task<bool> ensure_deps(Session& session,
const std::string& directory,
const std::vector<std::string>& arguments,
std::pair<std::string, uint32_t>& pch,
std::unordered_map<std::string, std::string>& pcms);
kota::task<bool> ensure_pch(Session& session,
const std::string& directory,
const std::vector<std::string>& arguments);
et::task<bool> ensure_pch(Session& session,
const std::string& directory,
const std::vector<std::string>& arguments);
bool is_stale(const Session& session);
void record_deps(Session& session, llvm::ArrayRef<std::string> deps);
void publish_diagnostics(const std::string& uri,
int version,
const kota::codec::RawValue& diags);
void publish_diagnostics(const std::string& uri, int version, const et::serde::RawValue& diags);
std::optional<HeaderFileContext> resolve_header_context(std::uint32_t header_path_id,
Session* session);
@@ -124,8 +122,8 @@ private:
Session* session);
private:
kota::event_loop& loop;
kota::ipc::JsonPeer& peer;
et::event_loop& loop;
et::ipc::JsonPeer& peer;
Workspace& workspace;
WorkerPool& pool;
llvm::DenseMap<std::uint32_t, Session>& sessions;

View File

@@ -1,197 +1,98 @@
#include "server/config.h"
#include <algorithm>
#include <thread>
#include "eventide/serde/toml.h"
#include "support/filesystem.h"
#include "support/glob_pattern.h"
#include "support/logging.h"
#include "kota/async/io/system.h"
#include "kota/codec/json/json.h"
#include "kota/codec/toml/toml.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/Process.h"
#include "llvm/Support/xxhash.h"
namespace clice {
/// Replace all occurrences of ${workspace} with the workspace root.
/// No-op when workspace_root is empty, to avoid producing paths like "/cache"
/// from "${workspace}/cache".
static void substitute_workspace(std::string& value, llvm::StringRef workspace_root) {
if(workspace_root.empty())
return;
static void substitute_workspace(std::string& value, const std::string& workspace_root) {
constexpr std::string_view placeholder = "${workspace}";
std::size_t pos = 0;
std::string::size_type pos = 0;
while((pos = value.find(placeholder, pos)) != std::string::npos) {
value.replace(pos, placeholder.size(), workspace_root);
pos += workspace_root.size();
}
}
/// Try to resolve the default cache directory using XDG_CACHE_HOME.
/// Returns empty string on failure.
static std::string resolve_xdg_cache_dir(llvm::StringRef workspace_root) {
// Determine base: $XDG_CACHE_HOME or ~/.cache
std::string base;
if(auto xdg = llvm::sys::Process::GetEnv("XDG_CACHE_HOME"); xdg && !xdg->empty()) {
base = std::move(*xdg);
} else if(auto home = llvm::sys::Process::GetEnv("HOME"); home && !home->empty()) {
base = path::join(*home, ".cache");
} else {
return {};
void CliceConfig::apply_defaults(const std::string& workspace_root) {
auto cpu_count = std::thread::hardware_concurrency();
if(cpu_count == 0)
cpu_count = 4;
if(stateful_worker_count == 0) {
stateful_worker_count = 2;
}
if(stateless_worker_count == 0) {
stateless_worker_count = 3;
}
if(worker_memory_limit == 0) {
worker_memory_limit = 4ULL * 1024 * 1024 * 1024; // 4GB default
}
if(cache_dir.empty() && !workspace_root.empty()) {
cache_dir = path::join(workspace_root, ".clice");
}
// Use a hash of workspace_root to create a unique subdirectory.
auto hash = llvm::xxh3_64bits(workspace_root);
auto dir = path::join(base, "clice", std::format("{:016x}", hash));
if(auto ec = llvm::sys::fs::create_directories(dir)) {
LOG_WARN("Failed to create XDG cache directory {}: {}", dir, ec.message());
return {};
if(index_dir.empty() && !cache_dir.empty()) {
index_dir = path::join(cache_dir, "index");
}
return dir;
if(logging_dir.empty() && !cache_dir.empty()) {
logging_dir = path::join(cache_dir, "logs");
}
// Apply variable substitution to string fields
substitute_workspace(compile_commands_path, workspace_root);
substitute_workspace(cache_dir, workspace_root);
substitute_workspace(index_dir, workspace_root);
substitute_workspace(logging_dir, workspace_root);
}
void Config::apply_defaults(llvm::StringRef workspace_root) {
auto& p = project;
if(p.max_active_file == 0)
p.max_active_file = 8;
if(!p.enable_indexing)
p.enable_indexing = true;
if(!p.idle_timeout_ms)
p.idle_timeout_ms = 3000;
if(p.stateful_worker_count == 0)
p.stateful_worker_count = 2;
if(p.stateless_worker_count == 0) {
auto cores = kota::sys::parallelism();
p.stateless_worker_count = std::max(cores / 2, 2u);
}
if(p.worker_memory_limit == 0)
p.worker_memory_limit = 4ULL * 1024 * 1024 * 1024; // 4GB
if(p.cache_dir.empty() && !workspace_root.empty()) {
p.cache_dir = resolve_xdg_cache_dir(workspace_root);
if(p.cache_dir.empty())
p.cache_dir = path::join(workspace_root, ".clice");
}
if(p.index_dir.empty() && !p.cache_dir.empty())
p.index_dir = path::join(p.cache_dir, "index");
if(p.logging_dir.empty() && !p.cache_dir.empty())
p.logging_dir = path::join(p.cache_dir, "logs");
// Variable substitution on string fields.
substitute_workspace(p.cache_dir, workspace_root);
substitute_workspace(p.index_dir, workspace_root);
substitute_workspace(p.logging_dir, workspace_root);
for(auto& entry: p.compile_commands_paths)
substitute_workspace(entry, workspace_root);
// Pre-compile glob patterns from rules.
compiled_rules.clear();
for(auto& rule: rules) {
CompiledRule compiled;
for(auto& pattern_str: rule.patterns) {
auto pat = GlobPattern::create(pattern_str);
if(!pat) {
LOG_WARN("Invalid glob pattern in rule: {}", pattern_str);
continue;
}
compiled.patterns.push_back(std::move(*pat));
}
// Drop the whole rule if no pattern compiled successfully — otherwise the
// append/remove flags would be silently attached to a rule that can never match.
if(compiled.patterns.empty()) {
if(!rule.patterns.empty())
LOG_WARN("Rule dropped: all glob patterns failed to compile");
continue;
}
compiled.append.assign(rule.append.begin(), rule.append.end());
compiled.remove.assign(rule.remove.begin(), rule.remove.end());
compiled_rules.push_back(std::move(compiled));
}
}
void Config::match_rules(llvm::StringRef file_path,
std::vector<std::string>& append,
std::vector<std::string>& remove) const {
// Rules are processed in declaration order so that a later rule can
// override an earlier one. Specifically, when a later rule removes
// an argument, we also strip any string-equal entry already added
// to `append` by an earlier matching rule — otherwise the append
// would silently survive (lookup applies removes to the base flags
// only, not to entries contributed via `append`).
for(auto& rule: compiled_rules) {
bool matched =
std::ranges::any_of(rule.patterns, [&](auto& pat) { return pat.match(file_path); });
if(!matched)
continue;
for(auto& r: rule.remove) {
std::erase(append, r);
remove.push_back(r);
}
append.insert(append.end(), rule.append.begin(), rule.append.end());
}
}
std::optional<Config> Config::load(llvm::StringRef path, llvm::StringRef workspace_root) {
std::optional<CliceConfig> CliceConfig::load(const std::string& path,
const std::string& workspace_root) {
auto content = fs::read(path);
if(!content)
if(!content) {
return std::nullopt;
}
auto result = kota::codec::toml::parse<Config>(*content);
auto result = eventide::serde::toml::parse<CliceConfig>(*content);
if(!result) {
LOG_ERROR("Invalid clice.toml {}: {}", path, result.error().to_string());
LOG_WARN("Failed to parse config file {}", path);
return std::nullopt;
}
auto config = std::move(*result);
config.apply_defaults(workspace_root);
LOG_INFO("Loaded config from {}", path);
return config;
}
std::optional<Config> Config::load_from_json(llvm::StringRef json, llvm::StringRef workspace_root) {
auto result = kota::codec::json::from_json<Config>(json);
if(!result) {
LOG_WARN("Failed to parse initializationOptions JSON: {}", result.error().message());
return std::nullopt;
}
auto config = std::move(*result);
config.apply_defaults(workspace_root);
LOG_INFO("Loaded config from initializationOptions");
return config;
}
Config Config::load_from_workspace(llvm::StringRef workspace_root, std::string* warning) {
CliceConfig CliceConfig::load_from_workspace(const std::string& workspace_root) {
if(!workspace_root.empty()) {
// Try standard config file locations
for(auto* name: {"clice.toml", ".clice/config.toml"}) {
auto config_path = path::join(workspace_root, name);
if(!llvm::sys::fs::exists(config_path))
continue;
if(auto config = load(config_path, workspace_root))
return std::move(*config);
// Present but malformed: fall through to defaults, but surface
// the situation clearly so users know their config wasn't applied.
LOG_WARN("Falling back to default configuration because {} is invalid", config_path);
if(warning)
*warning = std::format("Configuration file {} is invalid, falling back to defaults",
config_path);
if(llvm::sys::fs::exists(config_path)) {
auto config = load(config_path, workspace_root);
if(config)
return std::move(*config);
}
}
}
Config config;
// No config file found; use defaults
CliceConfig config;
config.apply_defaults(workspace_root);
LOG_INFO(
"No clice.toml found, using default configuration " "(stateful={}, stateless={}, memory_limit={}MB)",
config.project.stateful_worker_count.value,
config.project.stateless_worker_count.value,
config.project.worker_memory_limit.value / (1024 * 1024));
config.stateful_worker_count,
config.stateless_worker_count,
config.worker_memory_limit / (1024 * 1024));
return config;
}

View File

@@ -3,80 +3,44 @@
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
#include "support/glob_pattern.h"
#include "kota/meta/annotation.h"
#include "llvm/ADT/StringRef.h"
namespace clice {
using kota::meta::defaulted;
/// Configuration for the clice LSP server, loadable from clice.toml.
struct CliceConfig {
// Worker configuration (0 = auto-detect from system resources)
std::uint32_t stateful_worker_count = 0;
std::uint32_t stateless_worker_count = 0;
std::uint64_t worker_memory_limit = 0; // bytes; 0 = auto
/// A file-pattern rule that appends/removes compilation flags.
/// Corresponds to `[[rules]]` in clice.toml.
struct ConfigRule {
defaulted<std::vector<std::string>> patterns;
defaulted<std::vector<std::string>> append;
defaulted<std::vector<std::string>> remove;
};
// Compilation database path (empty = auto-detect)
std::string compile_commands_path;
/// Corresponds to the `[project]` section in clice.toml.
struct ProjectConfig {
defaulted<bool> clang_tidy = {};
defaulted<int> max_active_file = {};
// Cache directory (empty = default: <workspace>/.clice/)
std::string cache_dir;
defaulted<std::string> cache_dir;
defaulted<std::string> index_dir;
defaulted<std::string> logging_dir;
// Index storage directory (default: <cache_dir>/index/)
std::string index_dir;
defaulted<std::vector<std::string>> compile_commands_paths;
// Logging directory (default: <cache_dir>/logs/)
std::string logging_dir;
std::optional<bool> enable_indexing;
std::optional<int> idle_timeout_ms;
defaulted<std::uint32_t> stateful_worker_count = {};
defaulted<std::uint32_t> stateless_worker_count = {};
defaulted<std::uint64_t> worker_memory_limit = {};
};
struct CompiledRule {
std::vector<GlobPattern> patterns;
std::vector<std::string> append;
std::vector<std::string> remove;
};
/// Configuration for the clice LSP server, loadable from clice.toml
/// or passed via LSP initializationOptions.
struct Config {
defaulted<ProjectConfig> project;
defaulted<std::vector<ConfigRule>> rules;
kota::meta::annotation<std::vector<CompiledRule>, kota::meta::attrs::skip> compiled_rules;
// Background indexing
bool enable_indexing = true;
int idle_timeout_ms = 3000;
/// Compute default values for any field left at its zero/empty sentinel.
void apply_defaults(llvm::StringRef workspace_root);
/// Collect append/remove flags from all rules whose patterns match `path`.
void match_rules(llvm::StringRef path,
std::vector<std::string>& append,
std::vector<std::string>& remove) const;
void apply_defaults(const std::string& workspace_root);
/// Try to load configuration from a TOML file.
static std::optional<Config> load(llvm::StringRef path, llvm::StringRef workspace_root);
/// Try to load configuration from a JSON string (e.g. initializationOptions).
static std::optional<Config> load_from_json(llvm::StringRef json,
llvm::StringRef workspace_root);
/// Performs ${workspace} variable substitution in string fields.
/// Returns std::nullopt if the file does not exist or cannot be parsed.
static std::optional<CliceConfig> load(const std::string& path,
const std::string& workspace_root);
/// Load config from the workspace, trying standard locations.
/// Returns a default config (with apply_defaults) if no file is found.
/// If `warning` is non-null and a config file was found but malformed,
/// the warning message is written there.
static Config load_from_workspace(llvm::StringRef workspace_root,
std::string* warning = nullptr);
static CliceConfig load_from_workspace(const std::string& workspace_root);
};
} // namespace clice

View File

@@ -1,10 +1,12 @@
#include "server/indexer.h"
#include <algorithm>
#include <string>
#include <variant>
#include <vector>
#include "eventide/ipc/lsp/position.h"
#include "eventide/ipc/lsp/protocol.h"
#include "eventide/ipc/lsp/uri.h"
#include "index/tu_index.h"
#include "server/compiler.h"
#include "server/protocol.h"
@@ -13,9 +15,6 @@
#include "support/filesystem.h"
#include "support/logging.h"
#include "kota/ipc/lsp/position.h"
#include "kota/ipc/lsp/protocol.h"
#include "kota/ipc/lsp/uri.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Path.h"
@@ -23,7 +22,7 @@
namespace clice {
namespace lsp = kota::ipc::lsp;
namespace lsp = eventide::ipc::lsp;
void Indexer::merge(const void* tu_index_data, std::size_t size) {
auto tu_index = index::TUIndex::from(tu_index_data);
@@ -625,106 +624,19 @@ void Indexer::enqueue(std::uint32_t server_path_id) {
index_queue.push_back(server_path_id);
}
void Indexer::pause_indexing() {
++pause_depth;
if(pause_depth == 1) {
resume_event.reset();
LOG_DEBUG("Background indexing paused");
}
}
void Indexer::resume_indexing() {
if(pause_depth > 0)
--pause_depth;
if(pause_depth == 0) {
resume_event.set();
LOG_DEBUG("Background indexing resumed");
}
}
void Indexer::schedule() {
if(!*workspace.config.project.enable_indexing || indexing_active || indexing_scheduled)
if(!workspace.config.enable_indexing || indexing_active || indexing_scheduled)
return;
indexing_scheduled = true;
if(!index_idle_timer) {
index_idle_timer = std::make_shared<kota::timer>(kota::timer::create(loop));
index_idle_timer = std::make_shared<et::timer>(et::timer::create(loop));
}
index_idle_timer->start(std::chrono::milliseconds(*workspace.config.project.idle_timeout_ms));
index_idle_timer->start(std::chrono::milliseconds(workspace.config.idle_timeout_ms));
loop.schedule(run_background_indexing());
}
kota::task<> Indexer::index_one(std::uint32_t server_path_id) {
auto file_path = std::string(workspace.path_pool.resolve(server_path_id));
if(sessions.contains(server_path_id))
co_return;
if(!need_update(file_path))
co_return;
// For module interface units, compile their PCM (and transitive deps)
// first so the stateless worker has the artifacts it needs.
if(workspace.compile_graph && workspace.path_to_module.contains(server_path_id)) {
co_await workspace.compile_graph->compile(server_path_id);
}
worker::BuildParams params;
params.kind = worker::BuildKind::Index;
params.file = file_path;
if(!compiler.fill_compile_args(file_path, params.directory, params.arguments, nullptr))
co_return;
workspace.fill_pcm_deps(params.pcms);
LOG_INFO("Background indexing: {}", file_path);
auto result = co_await pool.send_stateless(params);
if(result.has_value() && result.value().success && !result.value().tu_index_data.empty()) {
LOG_INFO("Background indexing got TUIndex for {}: {} bytes",
file_path,
result.value().tu_index_data.size());
merge(result.value().tu_index_data.data(), result.value().tu_index_data.size());
} else if(result.has_value() && !result.value().success) {
LOG_WARN("Background index failed for {}: {}", file_path, result.value().error);
} else if(result.has_value() && result.value().tu_index_data.empty()) {
LOG_WARN("Background index returned empty TUIndex for {}", file_path);
} else {
LOG_WARN("Background index IPC error for {}: {}", file_path, result.error().message);
}
}
kota::task<> Indexer::monitor_resources(std::uint32_t generation) {
while(generation == monitor_generation) {
co_await kota::sleep(std::chrono::milliseconds(3000), loop);
if(generation != monitor_generation)
break;
auto mem = kota::sys::memory();
if(mem.total == 0)
continue;
// Respect cgroup/container limits when present.
auto effective_total =
(mem.constrained > 0 && mem.constrained < mem.total) ? mem.constrained : mem.total;
auto ratio = static_cast<double>(mem.available) / static_cast<double>(effective_total);
if(ratio < 0.15 && max_concurrent > 1) {
--max_concurrent;
LOG_INFO("Index concurrency -> {} (memory pressure: {:.0f}% available)",
max_concurrent,
ratio * 100);
} else if(ratio > 0.30 && max_concurrent < baseline_concurrent) {
++max_concurrent;
LOG_DEBUG("Index concurrency -> {} (memory OK: {:.0f}% available)",
max_concurrent,
ratio * 100);
}
}
}
kota::task<> Indexer::run_background_indexing() {
et::task<> Indexer::run_background_indexing() {
if(index_idle_timer) {
co_await index_idle_timer->wait();
}
@@ -736,89 +648,49 @@ kota::task<> Indexer::run_background_indexing() {
}
indexing_active = true;
++monitor_generation;
loop.schedule(monitor_resources(monitor_generation));
std::size_t processed = 0;
// Put module interface units first so their PCMs are built before
// non-module files that might import them.
std::stable_partition(
index_queue.begin() + index_queue_pos,
index_queue.end(),
[this](std::uint32_t id) { return workspace.path_to_module.contains(id); });
while(index_queue_pos < index_queue.size()) {
auto server_path_id = index_queue[index_queue_pos];
index_queue_pos++;
auto batch = index_queue.size() - index_queue_pos;
std::size_t dispatched = 0;
std::size_t completed = 0;
finished = 0;
auto file_path = std::string(workspace.path_pool.resolve(server_path_id));
// Progress reporting via LSP $/progress.
std::optional<lsp::ProgressReporter<kota::ipc::JsonPeer>> progress;
if(peer) {
progress.emplace(*peer, protocol::ProgressToken(std::string("clice/backgroundIndex")));
auto create_result = co_await progress->create();
if(!create_result.has_error()) {
progress->begin("Indexing", std::format("0/{} files", batch), 0);
if(sessions.contains(server_path_id))
continue;
if(!need_update(file_path))
continue;
worker::BuildParams params;
params.kind = worker::BuildKind::Index;
params.file = file_path;
if(!compiler.fill_compile_args(file_path, params.directory, params.arguments, nullptr))
continue;
workspace.fill_pcm_deps(params.pcms);
LOG_INFO("Background indexing: {}", file_path);
auto result = co_await pool.send_stateless(params);
if(result.has_value() && result.value().success && !result.value().tu_index_data.empty()) {
LOG_INFO("Background indexing got TUIndex for {}: {} bytes",
file_path,
result.value().tu_index_data.size());
merge(result.value().tu_index_data.data(), result.value().tu_index_data.size());
++processed;
} else if(result.has_value() && !result.value().success) {
LOG_WARN("Background index failed for {}: {}", file_path, result.value().error);
} else if(result.has_value() && result.value().tu_index_data.empty()) {
LOG_WARN("Background index returned empty TUIndex for {}", file_path);
} else {
progress.reset();
LOG_WARN("Background index IPC error for {}: {}", file_path, result.error().message);
}
}
while(index_queue_pos < index_queue.size() || inflight > 0) {
// Dispatch new tasks up to max_concurrent.
while(index_queue_pos < index_queue.size() && inflight < max_concurrent) {
// Wait if paused by a user request.
if(pause_depth > 0) {
co_await resume_event.wait();
}
auto server_path_id = index_queue[index_queue_pos++];
// Quick pre-filter: skip open files and fresh files without
// consuming a concurrency slot.
auto file_path = std::string(workspace.path_pool.resolve(server_path_id));
if(sessions.contains(server_path_id) || !need_update(file_path)) {
++completed;
continue;
}
++inflight;
++dispatched;
// Launch the index task. On completion it decrements
// inflight, bumps finished, and signals the event.
loop.schedule([](Indexer* self, std::uint32_t id, kota::event& done) -> kota::task<> {
co_await self->index_one(id);
--self->inflight;
++self->finished;
done.set();
}(this, server_path_id, completion_event));
}
if(inflight == 0)
break;
// Wait for at least one task to finish.
co_await completion_event.wait();
completion_event.reset();
// Drain all completions that occurred since last wake.
completed += std::exchange(finished, 0);
// Report progress.
if(progress) {
auto pct = batch > 0 ? static_cast<std::uint32_t>(completed * 100 / batch) : 100;
progress->report(std::format("{}/{} files", completed, batch), pct);
}
}
if(progress) {
progress->end(std::format("Indexed {} files", dispatched));
}
indexing_active = false;
++monitor_generation; // Stop the monitor coroutine.
LOG_INFO("Background indexing complete: {} files dispatched", dispatched);
save(workspace.config.project.index_dir);
LOG_INFO("Background indexing complete: {} files processed", processed);
save(workspace.config.index_dir);
}
} // namespace clice

View File

@@ -7,23 +7,22 @@
#include <string>
#include <vector>
#include "eventide/async/async.h"
#include "eventide/ipc/lsp/position.h"
#include "eventide/ipc/lsp/protocol.h"
#include "semantic/relation_kind.h"
#include "semantic/symbol_kind.h"
#include "server/workspace.h"
#include "kota/async/async.h"
#include "kota/ipc/codec/json.h"
#include "kota/ipc/lsp/position.h"
#include "kota/ipc/lsp/progress.h"
#include "kota/ipc/lsp/protocol.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringRef.h"
namespace clice {
namespace protocol = kota::ipc::protocol;
namespace lsp = kota::ipc::lsp;
namespace et = eventide;
namespace protocol = et::ipc::protocol;
namespace lsp = et::ipc::lsp;
struct Session;
class Compiler;
@@ -55,7 +54,7 @@ struct SymbolInfo {
/// - Document lifecycle — handled by MasterServer
class Indexer {
public:
Indexer(kota::event_loop& loop,
Indexer(et::event_loop& loop,
Workspace& workspace,
llvm::DenseMap<std::uint32_t, Session>& sessions,
WorkerPool& pool,
@@ -64,47 +63,6 @@ public:
loop(loop), workspace(workspace), sessions(sessions), pool(pool), compiler(compiler),
is_file_open(std::move(is_file_open)) {}
/// Set the LSP peer for progress reporting. Must be called before
/// schedule() if progress notifications are desired.
void set_peer(kota::ipc::JsonPeer* p) {
peer = p;
}
/// Temporarily pause background indexing to give priority to user
/// requests. Indexing tasks already dispatched to workers continue,
/// but no new tasks will be sent until resume_indexing() is called.
void pause_indexing();
/// Resume background indexing after a pause.
void resume_indexing();
/// RAII guard that pauses indexing for its lifetime.
struct [[nodiscard]] ScopedPause {
Indexer& indexer;
explicit ScopedPause(Indexer& idx) : indexer(idx) {
indexer.pause_indexing();
}
~ScopedPause() {
indexer.resume_indexing();
}
ScopedPause(const ScopedPause&) = delete;
ScopedPause& operator=(const ScopedPause&) = delete;
};
ScopedPause scoped_pause() {
return ScopedPause{*this};
}
/// Set the maximum number of concurrent index tasks.
/// Also sets the baseline that dynamic adjustment will restore to.
void set_max_concurrency(std::size_t n) {
max_concurrent = std::max<std::size_t>(n, 1);
baseline_concurrent = max_concurrent;
}
/// Add a file to the background indexing queue.
void enqueue(std::uint32_t server_path_id);
@@ -207,7 +165,7 @@ private:
}
private:
kota::event_loop& loop;
et::event_loop& loop;
Workspace& workspace;
llvm::DenseMap<std::uint32_t, Session>& sessions;
WorkerPool& pool;
@@ -218,40 +176,14 @@ private:
/// server-path-id-keyed sessions map to project-level path_ids.
std::function<bool(std::uint32_t)> is_file_open;
/// LSP peer for progress reporting (optional, not owned).
kota::ipc::JsonPeer* peer = nullptr;
/// Background indexing queue and scheduling state.
std::vector<std::uint32_t> index_queue;
std::size_t index_queue_pos = 0;
bool indexing_active = false;
bool indexing_scheduled = false;
std::shared_ptr<kota::timer> index_idle_timer;
std::shared_ptr<et::timer> index_idle_timer;
/// Concurrency control for background indexing.
std::size_t max_concurrent = 2;
std::size_t baseline_concurrent = 2;
std::size_t inflight = 0;
std::size_t finished = 0; ///< Incremented by each completed dispatch task.
/// Pause/resume: when paused, new index tasks wait on this event.
/// Uses a counter so nested pause/resume pairs work correctly.
std::size_t pause_depth = 0;
kota::event resume_event{true};
/// Completion event — signalled by each finished dispatch task so the
/// main loop can wake up. Must be a member (not local to the coroutine)
/// because inflight tasks capture it by reference and may outlive the
/// coroutine frame during server shutdown.
kota::event completion_event;
/// Generation counter — incremented each run so a stale monitor_resources
/// coroutine can detect that its owning run has ended.
std::uint32_t monitor_generation = 0;
kota::task<> run_background_indexing();
kota::task<> index_one(std::uint32_t server_path_id);
kota::task<> monitor_resources(std::uint32_t generation);
et::task<> run_background_indexing();
};
} // namespace clice

View File

@@ -6,39 +6,37 @@
#include <type_traits>
#include <variant>
#include "eventide/ipc/lsp/position.h"
#include "eventide/ipc/lsp/protocol.h"
#include "eventide/ipc/lsp/uri.h"
#include "eventide/reflection/enum.h"
#include "eventide/serde/json/json.h"
#include "semantic/symbol_kind.h"
#include "server/protocol.h"
#include "support/filesystem.h"
#include "support/logging.h"
#include "kota/codec/json/json.h"
#include "kota/ipc/lsp/position.h"
#include "kota/ipc/lsp/protocol.h"
#include "kota/ipc/lsp/uri.h"
#include "kota/meta/enum.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/Process.h"
namespace clice {
namespace protocol = kota::ipc::protocol;
namespace lsp = kota::ipc::lsp;
namespace refl = kota::meta;
using kota::ipc::RequestResult;
using RequestContext = kota::ipc::JsonPeer::RequestContext;
using serde_raw = kota::codec::RawValue;
namespace protocol = eventide::ipc::protocol;
namespace lsp = eventide::ipc::lsp;
namespace refl = eventide::refl;
using et::ipc::RequestResult;
using RequestContext = et::ipc::JsonPeer::RequestContext;
using serde_raw = et::serde::RawValue;
/// Serialize a value to a JSON RawValue using LSP config.
template <typename T>
static serde_raw to_raw(const T& value) {
auto json = kota::codec::json::to_json<kota::ipc::lsp_config>(value);
auto json = et::serde::json::to_json<et::ipc::lsp_config>(value);
return serde_raw{json ? std::move(*json) : "null"};
}
MasterServer::MasterServer(kota::event_loop& loop,
kota::ipc::JsonPeer& peer,
std::string self_path) :
MasterServer::MasterServer(et::event_loop& loop, et::ipc::JsonPeer& peer, std::string self_path) :
loop(loop), peer(peer), pool(loop), compiler(loop, peer, workspace, pool, sessions),
indexer(loop,
workspace,
@@ -56,93 +54,63 @@ MasterServer::MasterServer(kota::event_loop& loop,
MasterServer::~MasterServer() = default;
void MasterServer::load_workspace() {
et::task<> MasterServer::load_workspace() {
if(workspace_root.empty())
return;
co_return;
auto& cfg = workspace.config.project;
if(!cfg.cache_dir.empty()) {
auto ec = llvm::sys::fs::create_directories(cfg.cache_dir);
if(!workspace.config.cache_dir.empty()) {
auto ec = llvm::sys::fs::create_directories(workspace.config.cache_dir);
if(ec) {
LOG_WARN("Failed to create cache directory {}: {}",
std::string_view(cfg.cache_dir),
workspace.config.cache_dir,
ec.message());
} else {
LOG_INFO("Cache directory: {}", std::string_view(cfg.cache_dir));
LOG_INFO("Cache directory: {}", workspace.config.cache_dir);
}
for(auto* subdir: {"cache/pch", "cache/pcm"}) {
auto dir = path::join(cfg.cache_dir, subdir);
if(auto ec2 = llvm::sys::fs::create_directories(dir))
auto dir = path::join(workspace.config.cache_dir, subdir);
auto ec2 = llvm::sys::fs::create_directories(dir);
if(ec2) {
LOG_WARN("Failed to create {}: {}", dir, ec2.message());
}
}
// Clean up stale files first, then load — load_cache() only restores
// entries still listed in cache.json, so cleanup won't delete live files.
workspace.cleanup_cache();
workspace.load_cache();
}
// Discover compile_commands.json: configured paths first, then auto-scan.
std::string cdb_path;
for(auto& configured: cfg.compile_commands_paths) {
// Each entry can be a file or a directory containing compile_commands.json.
if(llvm::sys::fs::is_directory(configured)) {
auto candidate = path::join(configured, "compile_commands.json");
if(llvm::sys::fs::exists(candidate)) {
cdb_path = std::move(candidate);
break;
}
} else if(llvm::sys::fs::exists(configured)) {
cdb_path = configured;
break;
if(!workspace.config.compile_commands_path.empty()) {
if(llvm::sys::fs::exists(workspace.config.compile_commands_path)) {
cdb_path = workspace.config.compile_commands_path;
} else {
LOG_WARN("Configured compile_commands_path not found: {}", configured);
LOG_WARN("Configured compile_commands_path not found: {}",
workspace.config.compile_commands_path);
}
}
// Auto-scan: workspace root + all immediate subdirectories.
if(cdb_path.empty()) {
auto try_candidate = [&](llvm::StringRef dir) -> bool {
auto candidate = path::join(dir, "compile_commands.json");
for(auto* subdir: {"build", "cmake-build-debug", "cmake-build-release", "out", "."}) {
auto candidate = path::join(workspace_root, subdir, "compile_commands.json");
if(llvm::sys::fs::exists(candidate)) {
cdb_path = std::move(candidate);
return true;
}
return false;
};
if(!try_candidate(workspace_root)) {
std::error_code ec;
for(llvm::sys::fs::directory_iterator it(workspace_root, ec), end; it != end && !ec;
it.increment(ec)) {
if(it->type() == llvm::sys::fs::file_type::directory_file) {
if(try_candidate(it->path()))
break;
}
break;
}
}
}
if(cdb_path.empty()) {
LOG_WARN("No compile_commands.json found in workspace {}", workspace_root);
peer.send_notification(protocol::LogMessageParams{
protocol::MessageType::Warning,
std::format("No compile_commands.json found in workspace {}", workspace_root)});
return;
co_return;
}
auto count = workspace.cdb.load(cdb_path);
LOG_INFO("Loaded CDB from {} with {} entries", cdb_path, count);
auto report = scan_dependency_graph(workspace.cdb,
workspace.path_pool,
workspace.dep_graph,
/*cache=*/nullptr,
[this](llvm::StringRef path,
std::vector<std::string>& append,
std::vector<std::string>& remove) {
workspace.config.match_rules(path, append, remove);
});
auto report = scan_dependency_graph(workspace.cdb, workspace.path_pool, workspace.dep_graph);
workspace.dep_graph.build_reverse_map();
auto unresolved = report.includes_found - report.includes_resolved;
@@ -161,13 +129,14 @@ void MasterServer::load_workspace() {
report.includes_found,
accuracy,
report.waves);
if(unresolved > 0)
if(unresolved > 0) {
LOG_WARN("{} unresolved includes", unresolved);
}
workspace.build_module_map();
indexer.load(cfg.index_dir);
indexer.load(workspace.config.index_dir);
if(*cfg.enable_indexing) {
if(workspace.config.enable_indexing) {
for(auto& entry: workspace.cdb.get_entries()) {
auto file = workspace.cdb.resolve_path(entry.file);
auto server_id = workspace.path_pool.intern(file);
@@ -185,7 +154,7 @@ void MasterServer::register_handlers() {
peer.on_request([this](RequestContext& ctx, const protocol::InitializeParams& params)
-> RequestResult<protocol::InitializeParams> {
if(lifecycle != ServerLifecycle::Uninitialized) {
co_return kota::outcome_error(protocol::Error{"Server already initialized"});
co_return et::outcome_error(protocol::Error{"Server already initialized"});
}
auto& init = params.lsp__initialize_params;
@@ -193,14 +162,6 @@ void MasterServer::register_handlers() {
workspace_root = uri_to_path(*init.root_uri);
}
// Capture initializationOptions as raw JSON for config loading.
if(init.initialization_options.has_value()) {
auto json =
kota::codec::json::to_json<kota::ipc::lsp_config>(*init.initialization_options);
if(json)
init_options_json = std::move(*json);
}
lifecycle = ServerLifecycle::Initialized;
LOG_INFO("Initialized with workspace: {}", workspace_root);
@@ -281,60 +242,30 @@ void MasterServer::register_handlers() {
});
peer.on_notification([this](const protocol::InitializedParams& params) {
// Config priority: initializationOptions > clice.toml > defaults.
// Load the workspace config (with defaults applied) first, then overlay
// any initializationOptions on top so fields not mentioned in the JSON
// keep the values from clice.toml — kotatsu's deserializer only touches
// fields that are present in the input.
std::string config_warning;
workspace.config = Config::load_from_workspace(workspace_root, &config_warning);
if(!config_warning.empty())
peer.send_notification(
protocol::LogMessageParams{protocol::MessageType::Warning, config_warning});
if(!init_options_json.empty()) {
if(auto ov = kota::codec::json::parse(init_options_json, workspace.config); !ov) {
LOG_WARN("Failed to apply initializationOptions: {}", ov.error().to_string());
peer.send_notification(protocol::LogMessageParams{
protocol::MessageType::Warning,
std::format("Failed to apply initializationOptions: {}",
ov.error().to_string())});
} else {
// Re-run apply_defaults so overridden strings get workspace
// substitution and `compiled_rules` is rebuilt if `rules`
// changed. Defaults are gated on zero/empty sentinels, so
// existing values from the overlay are preserved.
workspace.config.apply_defaults(workspace_root);
LOG_INFO("Applied initializationOptions overlay");
}
init_options_json.clear();
}
workspace.config = CliceConfig::load_from_workspace(workspace_root);
auto& cfg = workspace.config.project;
if(!cfg.logging_dir.empty()) {
if(!workspace.config.logging_dir.empty()) {
auto now = std::chrono::system_clock::now();
auto pid = llvm::sys::Process::getProcessId();
auto session_dir =
path::join(cfg.logging_dir, std::format("{:%Y-%m-%d_%H-%M-%S}_{}", now, pid));
auto session_dir = path::join(workspace.config.logging_dir,
std::format("{:%Y-%m-%d_%H-%M-%S}_{}", now, pid));
logging::file_logger("master", session_dir, logging::options);
session_log_dir = session_dir;
}
LOG_INFO("Server ready (stateful={}, stateless={}, idle={}ms)",
cfg.stateful_worker_count.value,
cfg.stateless_worker_count.value,
*cfg.idle_timeout_ms);
workspace.config.stateful_worker_count,
workspace.config.stateless_worker_count,
workspace.config.idle_timeout_ms);
WorkerPoolOptions pool_opts;
pool_opts.self_path = self_path;
pool_opts.stateful_count = cfg.stateful_worker_count;
pool_opts.stateless_count = cfg.stateless_worker_count;
pool_opts.worker_memory_limit = cfg.worker_memory_limit;
pool_opts.stateful_count = workspace.config.stateful_worker_count;
pool_opts.stateless_count = workspace.config.stateless_worker_count;
pool_opts.worker_memory_limit = workspace.config.worker_memory_limit;
pool_opts.log_dir = session_log_dir;
if(!pool.start(pool_opts)) {
LOG_ERROR("Failed to start worker pool");
peer.send_notification(protocol::LogMessageParams{protocol::MessageType::Error,
"Failed to start worker pool"});
return;
}
@@ -344,10 +275,7 @@ void MasterServer::register_handlers() {
indexer.schedule();
};
indexer.set_peer(&peer);
indexer.set_max_concurrency(cfg.stateless_worker_count.value);
load_workspace();
loop.schedule(load_workspace());
});
peer.on_request(
@@ -362,10 +290,10 @@ void MasterServer::register_handlers() {
lifecycle = ServerLifecycle::Exited;
LOG_INFO("Exit notification received");
indexer.save(workspace.config.project.index_dir);
indexer.save(workspace.config.index_dir);
workspace.save_cache();
loop.schedule([this]() -> kota::task<> {
loop.schedule([this]() -> et::task<> {
co_await pool.stop();
loop.stop();
}());
@@ -501,7 +429,7 @@ void MasterServer::register_handlers() {
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_await kota::fail("Document not open");
co_return serde_raw{"null"};
co_return co_await compiler.forward_query(worker::QueryKind::Hover,
sit->second,
params.text_document_position_params.position);
@@ -513,7 +441,7 @@ void MasterServer::register_handlers() {
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_await kota::fail("Document not open");
co_return serde_raw{"null"};
co_return co_await compiler.forward_query(worker::QueryKind::SemanticTokens, sit->second);
});
@@ -523,7 +451,7 @@ void MasterServer::register_handlers() {
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_await kota::fail("Document not open");
co_return serde_raw{"null"};
co_return co_await compiler.forward_query(worker::QueryKind::InlayHints,
sit->second,
{},
@@ -536,7 +464,7 @@ void MasterServer::register_handlers() {
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_await kota::fail("Document not open");
co_return serde_raw{"null"};
co_return co_await compiler.forward_query(worker::QueryKind::FoldingRange, sit->second);
});
@@ -546,7 +474,7 @@ void MasterServer::register_handlers() {
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_await kota::fail("Document not open");
co_return serde_raw{"null"};
co_return co_await compiler.forward_query(worker::QueryKind::DocumentSymbol, sit->second);
});
@@ -556,7 +484,7 @@ void MasterServer::register_handlers() {
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_await kota::fail("Document not open");
co_return serde_raw{"null"};
auto& session = sit->second;
auto result = co_await compiler.forward_query(worker::QueryKind::DocumentLink, session);
if(!result.has_value())
@@ -589,7 +517,7 @@ void MasterServer::register_handlers() {
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_await kota::fail("Document not open");
co_return serde_raw{"null"};
co_return co_await compiler.forward_query(worker::QueryKind::CodeAction, sit->second);
});
@@ -644,7 +572,7 @@ void MasterServer::register_handlers() {
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_await kota::fail("Document not open");
co_return serde_raw{"null"};
co_return co_await compiler.forward_query(worker::QueryKind::GoToDefinition,
sit->second,
pos);
@@ -686,33 +614,28 @@ void MasterServer::register_handlers() {
/// Feature requests — stateless forwarding.
peer.on_request(
[this](RequestContext& ctx, const protocol::CompletionParams& params) -> RawResult {
auto path = uri_to_path(params.text_document_position_params.text_document.uri);
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_await kota::fail("Document not open");
auto pause = indexer.scoped_pause();
auto result =
co_await compiler.handle_completion(params.text_document_position_params.position,
sit->second);
co_return std::move(result);
});
peer.on_request([this](RequestContext& ctx,
const protocol::SignatureHelpParams& params) -> RawResult {
const protocol::CompletionParams& params) -> RawResult {
auto path = uri_to_path(params.text_document_position_params.text_document.uri);
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_await kota::fail("Document not open");
auto pause = indexer.scoped_pause();
auto result = co_await compiler.forward_build(worker::BuildKind::SignatureHelp,
co_return serde_raw{"null"};
co_return co_await compiler.handle_completion(params.text_document_position_params.position,
sit->second);
});
peer.on_request(
[this](RequestContext& ctx, const protocol::SignatureHelpParams& params) -> RawResult {
auto path = uri_to_path(params.text_document_position_params.text_document.uri);
auto path_id = workspace.path_pool.intern(path);
auto sit = sessions.find(path_id);
if(sit == sessions.end())
co_return serde_raw{"null"};
co_return co_await compiler.forward_build(worker::BuildKind::SignatureHelp,
params.text_document_position_params.position,
sit->second);
co_return std::move(result);
});
});
/// Hierarchy queries — index-based.
@@ -738,8 +661,10 @@ void MasterServer::register_handlers() {
const protocol::CallHierarchyIncomingCallsParams& params) -> RawResult {
auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
if(!info)
co_await kota::fail("Failed to resolve call hierarchy item");
co_return serde_raw{"null"};
auto results = indexer.find_incoming_calls(info->hash);
if(results.empty())
co_return serde_raw{"null"};
co_return to_raw(results);
});
@@ -748,8 +673,10 @@ void MasterServer::register_handlers() {
const protocol::CallHierarchyOutgoingCallsParams& params) -> RawResult {
auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
if(!info)
co_await kota::fail("Failed to resolve call hierarchy item");
co_return serde_raw{"null"};
auto results = indexer.find_outgoing_calls(info->hash);
if(results.empty())
co_return serde_raw{"null"};
co_return to_raw(results);
});
@@ -776,8 +703,10 @@ void MasterServer::register_handlers() {
const protocol::TypeHierarchySupertypesParams& params) -> RawResult {
auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
if(!info)
co_await kota::fail("Failed to resolve type hierarchy item");
co_return serde_raw{"null"};
auto results = indexer.find_supertypes(info->hash);
if(results.empty())
co_return serde_raw{"null"};
co_return to_raw(results);
});
@@ -786,14 +715,18 @@ void MasterServer::register_handlers() {
const protocol::TypeHierarchySubtypesParams& params) -> RawResult {
auto info = resolve_item(params.item.uri, params.item.range, params.item.data);
if(!info)
co_await kota::fail("Failed to resolve type hierarchy item");
co_return serde_raw{"null"};
auto results = indexer.find_subtypes(info->hash);
if(results.empty())
co_return serde_raw{"null"};
co_return to_raw(results);
});
peer.on_request(
[this](RequestContext& ctx, const protocol::WorkspaceSymbolParams& params) -> RawResult {
auto results = indexer.search_symbols(params.query);
if(results.empty())
co_return serde_raw{"null"};
co_return to_raw(results);
});

View File

@@ -5,19 +5,21 @@
#include <string>
#include <vector>
#include "eventide/async/async.h"
#include "eventide/ipc/peer.h"
#include "eventide/serde/serde/raw_value.h"
#include "server/compiler.h"
#include "server/indexer.h"
#include "server/session.h"
#include "server/worker_pool.h"
#include "server/workspace.h"
#include "kota/async/async.h"
#include "kota/codec/json/json.h"
#include "kota/ipc/peer.h"
#include "llvm/ADT/DenseMap.h"
namespace clice {
namespace et = eventide;
enum class ServerLifecycle : std::uint8_t {
Uninitialized,
Initialized,
@@ -42,14 +44,14 @@ enum class ServerLifecycle : std::uint8_t {
/// point to disk files. The only path from Session to Workspace is didSave.
class MasterServer {
public:
MasterServer(kota::event_loop& loop, kota::ipc::JsonPeer& peer, std::string self_path);
MasterServer(et::event_loop& loop, et::ipc::JsonPeer& peer, std::string self_path);
~MasterServer();
void register_handlers();
private:
kota::event_loop& loop;
kota::ipc::JsonPeer& peer;
et::event_loop& loop;
et::ipc::JsonPeer& peer;
/// Persistent project-wide state (config, CDB, path pool, dependency
/// graphs, compilation caches, symbol index).
@@ -71,11 +73,10 @@ private:
std::string self_path;
std::string workspace_root;
std::string session_log_dir;
std::string init_options_json; ///< Raw JSON from initializationOptions, consumed once.
void load_workspace();
et::task<> load_workspace();
using RawResult = kota::task<kota::codec::RawValue, kota::ipc::Error>;
using RawResult = et::task<et::serde::RawValue, et::ipc::Error>;
};
} // namespace clice

View File

@@ -7,15 +7,14 @@
#include <utility>
#include <vector>
#include "eventide/ipc/lsp/protocol.h"
#include "eventide/ipc/protocol.h"
#include "eventide/serde/serde/raw_value.h"
#include "syntax/token.h"
#include "kota/codec/json/json.h"
#include "kota/ipc/lsp/protocol.h"
#include "kota/ipc/protocol.h"
namespace clice::worker {
namespace protocol = kota::ipc::protocol;
namespace protocol = eventide::ipc::protocol;
/// Kind of AST query dispatched to a stateful worker.
enum class QueryKind : uint8_t {
@@ -52,7 +51,7 @@ struct CompileParams {
struct CompileResult {
int version;
/// Diagnostics serialized as JSON (RawValue) to avoid bincode/serde annotation conflicts.
kota::codec::RawValue diagnostics;
eventide::serde::RawValue diagnostics;
std::size_t memory_usage;
std::vector<std::string> deps;
/// Serialized TUIndex for the main file (interested_only=true).
@@ -103,8 +102,8 @@ struct BuildResult {
std::string output_path; ///< PCH or PCM path
std::vector<std::string> deps;
std::string tu_index_data;
std::string pch_links_json; ///< Pre-serialized DocumentLink[] from PCH
kota::codec::RawValue result_json; ///< Completion/SignatureHelp result
std::string pch_links_json; ///< Pre-serialized DocumentLink[] from PCH
eventide::serde::RawValue result_json; ///< Completion/SignatureHelp result
};
struct DocumentUpdateParams {
@@ -159,7 +158,7 @@ struct SwitchContextResult {
} // namespace clice::ext
namespace kota::ipc::protocol {
namespace eventide::ipc::protocol {
template <>
struct RequestTraits<clice::worker::CompileParams> {
@@ -169,7 +168,7 @@ struct RequestTraits<clice::worker::CompileParams> {
template <>
struct RequestTraits<clice::worker::QueryParams> {
using Result = kota::codec::RawValue;
using Result = eventide::serde::RawValue;
constexpr inline static std::string_view method = "clice/worker/query";
};
@@ -194,4 +193,4 @@ struct NotificationTraits<clice::worker::EvictedParams> {
constexpr inline static std::string_view method = "clice/worker/evicted";
};
} // namespace kota::ipc::protocol
} // namespace eventide::ipc::protocol

View File

@@ -5,13 +5,15 @@
#include <optional>
#include <string>
#include "eventide/async/async.h"
#include "server/workspace.h"
#include "kota/async/async.h"
#include "llvm/ADT/SmallVector.h"
namespace clice {
namespace et = eventide;
/// An editing session for a single file opened in the editor.
///
/// Design principle: open files are never depended upon by other files.
@@ -43,7 +45,7 @@ struct Session {
/// Other queries wait on the event; the compilation task itself
/// runs independently and cannot be cancelled by LSP $/cancelRequest.
struct PendingCompile {
kota::event done;
et::event done;
bool succeeded = false;
};

View File

@@ -8,23 +8,23 @@
#include <vector>
#include "compile/compilation.h"
#include "eventide/async/async.h"
#include "eventide/ipc/peer.h"
#include "eventide/ipc/transport.h"
#include "feature/feature.h"
#include "index/tu_index.h"
#include "server/protocol.h"
#include "server/worker_common.h"
#include "support/logging.h"
#include "kota/async/async.h"
#include "kota/ipc/codec/bincode.h"
#include "kota/ipc/peer.h"
#include "kota/ipc/transport.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/Support/raw_ostream.h"
namespace clice {
using kota::ipc::RequestResult;
using RequestContext = kota::ipc::BincodePeer::RequestContext;
namespace et = eventide;
using et::ipc::RequestResult;
using RequestContext = et::ipc::BincodePeer::RequestContext;
struct DocumentEntry {
int version = 0;
@@ -35,7 +35,7 @@ struct DocumentEntry {
// Signaled when the first compilation completes (has_ast becomes true).
// Feature handlers co_await this before accessing the AST.
kota::event ast_ready{false};
et::event ast_ready{false};
// Compilation context (from CompileParams)
std::string directory;
@@ -44,11 +44,11 @@ struct DocumentEntry {
llvm::StringMap<std::string> pcms;
// Per-document serialization mutex
kota::mutex strand;
et::mutex strand;
};
class StatefulWorker {
kota::ipc::BincodePeer& peer;
et::ipc::BincodePeer& peer;
std::uint64_t memory_limit;
llvm::StringMap<std::shared_ptr<DocumentEntry>> documents;
@@ -91,11 +91,10 @@ class StatefulWorker {
/// Look up document, wait for AST, lock strand, run fn(doc) on thread pool, unlock.
/// Returns "null" if document not found or AST not usable.
template <typename F>
kota::task<kota::codec::RawValue> with_ast(llvm::StringRef path, F&& fn) {
et::task<et::serde::RawValue> with_ast(llvm::StringRef path, F&& fn) {
auto it = documents.find(path);
if(it == documents.end()) {
LOG_WARN("with_ast: document not found: {}", path.str());
co_return kota::codec::RawValue{"null"};
co_return et::serde::RawValue{"null"};
}
// Hold shared_ptr so Evict can't destroy the entry mid-request.
@@ -105,11 +104,9 @@ class StatefulWorker {
co_await doc->ast_ready.wait();
co_await doc->strand.lock();
auto result = co_await kota::queue([&]() -> kota::codec::RawValue {
if(!doc->has_ast || (!doc->unit.completed() && !doc->unit.fatal_error())) {
LOG_WARN("with_ast: AST not available for {}", path.str());
return kota::codec::RawValue{"null"};
}
auto result = co_await et::queue([&]() -> et::serde::RawValue {
if(!doc->has_ast || (!doc->unit.completed() && !doc->unit.fatal_error()))
return et::serde::RawValue{"null"};
return fn(*doc);
});
@@ -118,7 +115,7 @@ class StatefulWorker {
}
public:
StatefulWorker(kota::ipc::BincodePeer& peer, std::uint64_t memory_limit) :
StatefulWorker(et::ipc::BincodePeer& peer, std::uint64_t memory_limit) :
peer(peer), memory_limit(memory_limit) {}
void register_handlers();
@@ -150,7 +147,7 @@ void StatefulWorker::register_handlers() {
doc->pcms.try_emplace(name, pcm_path);
}
auto compile_result = co_await kota::queue([&]() -> worker::CompileResult {
auto compile_result = co_await et::queue([&]() -> worker::CompileResult {
ScopedTimer timer;
CompilationParams cp;
@@ -172,15 +169,15 @@ void StatefulWorker::register_handlers() {
result.version = doc->version;
if(doc->unit.completed() || doc->unit.fatal_error()) {
auto diags = feature::diagnostics(doc->unit);
auto json = kota::codec::json::to_json<kota::ipc::lsp_config>(diags);
result.diagnostics = kota::codec::RawValue{json ? std::move(*json) : "[]"};
auto json = et::serde::json::to_json<et::ipc::lsp_config>(diags);
result.diagnostics = et::serde::RawValue{json ? std::move(*json) : "[]"};
LOG_INFO("Compile done: path={}, {}ms, {} diags, fatal={}",
params.path,
timer.ms(),
diags.size(),
doc->unit.fatal_error());
} else {
result.diagnostics = kota::codec::RawValue{"[]"};
result.diagnostics = et::serde::RawValue{"[]"};
LOG_WARN("Compile incomplete: path={}, {}ms", params.path, timer.ms());
}
result.memory_usage = 0; // TODO: query actual memory
@@ -204,7 +201,7 @@ void StatefulWorker::register_handlers() {
// === DocumentUpdate ===
// Only mark the document dirty — do NOT update doc.text or doc.version
// here. The kota::queue compilation work may be reading doc.text on the
// here. The et::queue compilation work may be reading doc.text on the
// thread pool concurrently, so writing it from the event loop would be
// a data race. The next Compile request will bring the correct text
// and update it inside the strand lock.
@@ -241,11 +238,11 @@ void StatefulWorker::register_handlers() {
case K::Hover:
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
auto result = feature::hover(doc.unit, params.offset);
return result ? to_raw(*result) : kota::codec::RawValue{"null"};
return result ? to_raw(*result) : et::serde::RawValue{"null"};
});
case K::GoToDefinition:
// TODO: Implement go-to-definition
co_return kota::codec::RawValue{"[]"};
co_return et::serde::RawValue{"[]"};
case K::SemanticTokens:
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
return to_raw(feature::semantic_tokens(doc.unit));
@@ -271,9 +268,9 @@ void StatefulWorker::register_handlers() {
});
case K::CodeAction:
// TODO: Implement code actions
co_return kota::codec::RawValue{"[]"};
co_return et::serde::RawValue{"[]"};
}
co_return kota::codec::RawValue{"null"};
co_return et::serde::RawValue{"null"};
});
}
@@ -287,15 +284,15 @@ int run_stateful_worker_mode(std::uint64_t memory_limit,
LOG_INFO("Starting stateful worker, memory_limit={}MB", memory_limit / (1024 * 1024));
kota::event_loop loop;
et::event_loop loop;
auto transport_result = kota::ipc::StreamTransport::open_stdio(loop);
auto transport_result = et::ipc::StreamTransport::open_stdio(loop);
if(!transport_result) {
LOG_ERROR("Failed to open stdio transport");
return 1;
}
kota::ipc::BincodePeer peer(loop, std::move(*transport_result));
et::ipc::BincodePeer peer(loop, std::move(*transport_result));
StatefulWorker worker(peer, memory_limit);
worker.register_handlers();

View File

@@ -1,38 +1,22 @@
#include "server/stateless_worker.h"
#include "compile/compilation.h"
#include "eventide/async/async.h"
#include "eventide/ipc/peer.h"
#include "eventide/ipc/transport.h"
#include "feature/feature.h"
#include "index/tu_index.h"
#include "server/protocol.h"
#include "server/worker_common.h"
#include "support/logging.h"
#include "kota/async/async.h"
#include "kota/ipc/codec/bincode.h"
#include "kota/ipc/peer.h"
#include "kota/ipc/transport.h"
#include "llvm/Support/raw_ostream.h"
namespace clice {
/// RAII guard that lowers the current process's scheduling priority and
/// restores it on destruction.
struct ScopedNice {
int saved;
explicit ScopedNice(int increment = 10) {
auto p = kota::sys::priority();
saved = p ? *p : 0;
kota::sys::set_priority(saved + increment);
}
~ScopedNice() {
kota::sys::set_priority(saved);
}
};
using kota::ipc::RequestResult;
using RequestContext = kota::ipc::BincodePeer::RequestContext;
namespace et = eventide;
using et::ipc::RequestResult;
using RequestContext = et::ipc::BincodePeer::RequestContext;
/// Extract error messages from compilation diagnostics.
static std::string collect_errors(CompilationUnit& unit) {
@@ -244,8 +228,6 @@ static worker::BuildResult handle_completion(const worker::BuildParams& params)
cp.completion = {params.file, params.offset};
auto items = feature::code_complete(cp);
if(items.empty())
LOG_DEBUG("Completion: no items returned for {}:{}", params.file, params.offset);
LOG_DEBUG("Completion done: {} items, {}ms", items.size(), timer.ms());
worker::BuildResult result;
@@ -269,7 +251,7 @@ static worker::BuildResult handle_signature_help(const worker::BuildParams& para
cp.completion = {params.file, params.offset};
auto help = feature::signature_help(cp);
LOG_DEBUG("SignatureHelp done: {} signatures, {}ms", help.signatures.size(), timer.ms());
LOG_DEBUG("SignatureHelp done: {}ms", timer.ms());
worker::BuildResult result;
result.result_json = to_raw(help);
@@ -284,27 +266,24 @@ int run_stateless_worker_mode(const std::string& worker_name, const std::string&
LOG_INFO("Starting stateless worker");
kota::event_loop loop;
et::event_loop loop;
auto transport_result = kota::ipc::StreamTransport::open_stdio(loop);
auto transport_result = et::ipc::StreamTransport::open_stdio(loop);
if(!transport_result) {
LOG_ERROR("Failed to open stdio transport");
return 1;
}
kota::ipc::BincodePeer peer(loop, std::move(*transport_result));
et::ipc::BincodePeer peer(loop, std::move(*transport_result));
peer.on_request([&](RequestContext& ctx,
const worker::BuildParams& params) -> RequestResult<worker::BuildParams> {
using K = worker::BuildKind;
auto result = co_await kota::queue([&]() -> worker::BuildResult {
auto result = co_await et::queue([&]() -> worker::BuildResult {
switch(params.kind) {
case K::BuildPCH: return handle_build_pch(params);
case K::BuildPCM: return handle_build_pcm(params);
case K::Index: {
ScopedNice guard;
return handle_index(params);
}
case K::Index: return handle_index(params);
case K::Completion: return handle_completion(params);
case K::SignatureHelp: return handle_signature_help(params);
}

View File

@@ -7,9 +7,9 @@
#include <vector>
#include "compile/compilation.h"
#include "kota/codec/json/json.h"
#include "kota/ipc/codec/json.h"
#include "eventide/ipc/json_codec.h"
#include "eventide/serde/json/serializer.h"
#include "eventide/serde/serde/raw_value.h"
namespace clice {
@@ -36,9 +36,9 @@ inline void fill_args(CompilationParams& cp,
/// Serialize a value to JSON RawValue using LSP config.
template <typename T>
inline kota::codec::RawValue to_raw(const T& value) {
auto json = kota::codec::json::to_json<kota::ipc::lsp_config>(value);
return kota::codec::RawValue{json ? std::move(*json) : "null"};
inline eventide::serde::RawValue to_raw(const T& value) {
auto json = eventide::serde::json::to_json<eventide::ipc::lsp_config>(value);
return eventide::serde::RawValue{json ? std::move(*json) : "null"};
}
} // namespace clice

View File

@@ -3,23 +3,23 @@
#include <csignal>
#include <string>
#include "eventide/ipc/transport.h"
#include "support/logging.h"
#include "kota/ipc/transport.h"
namespace clice {
namespace {
/// Coroutine that drains a worker's stderr pipe.
/// Workers write their own log files, so this only captures unexpected output
/// (crash stacktraces, assertion failures, sanitizer reports, etc.).
kota::task<> drain_stderr(kota::pipe stderr_pipe, std::string prefix) {
/// (crash stacktraces, assertion failures, etc.) that bypasses spdlog.
et::task<> drain_stderr(et::pipe stderr_pipe, std::string prefix) {
std::string buffer;
while(true) {
auto result = co_await stderr_pipe.read();
if(!result.has_value())
if(!result.has_value()) {
break;
}
auto& chunk = result.value();
if(chunk.empty())
break;
@@ -33,7 +33,7 @@ kota::task<> drain_stderr(kota::pipe stderr_pipe, std::string prefix) {
break;
auto line = buffer.substr(pos, nl - pos);
if(!line.empty()) {
LOG_WARN("{} {}", prefix, line);
LOG_DEBUG("{} {}", prefix, line);
}
pos = nl + 1;
}
@@ -41,7 +41,7 @@ kota::task<> drain_stderr(kota::pipe stderr_pipe, std::string prefix) {
}
if(!buffer.empty()) {
LOG_WARN("{} {}", prefix, buffer);
LOG_DEBUG("{} {}", prefix, buffer);
}
}
@@ -54,7 +54,7 @@ bool WorkerPool::spawn_worker(const std::string& self_path,
auto worker_index = workers.size();
std::string worker_name = std::string(stateful ? "SF-" : "SL-") + std::to_string(worker_index);
kota::process::options opts;
et::process::options opts;
opts.file = self_path;
if(stateful) {
opts.args = {self_path,
@@ -75,12 +75,12 @@ bool WorkerPool::spawn_worker(const std::string& self_path,
}
opts.streams = {
kota::process::stdio::pipe(true, false), // stdin: child reads
kota::process::stdio::pipe(false, true), // stdout: child writes
kota::process::stdio::pipe(false, true), // stderr: child writes
et::process::stdio::pipe(true, false), // stdin: child reads
et::process::stdio::pipe(false, true), // stdout: child writes
et::process::stdio::pipe(false, true), // stderr: child writes
};
auto result = kota::process::spawn(opts, loop);
auto result = et::process::spawn(opts, loop);
if(!result) {
LOG_ERROR("Failed to spawn {} worker: {}",
stateful ? "stateful" : "stateless",
@@ -92,9 +92,9 @@ bool WorkerPool::spawn_worker(const std::string& self_path,
// StreamTransport: input = child's stdout (parent reads), output = child's stdin (parent
// writes)
auto transport = std::make_unique<kota::ipc::StreamTransport>(std::move(spawn.stdout_pipe),
std::move(spawn.stdin_pipe));
auto peer = std::make_unique<kota::ipc::BincodePeer>(loop, std::move(transport));
auto transport = std::make_unique<et::ipc::StreamTransport>(std::move(spawn.stdout_pipe),
std::move(spawn.stdin_pipe));
auto peer = std::make_unique<et::ipc::BincodePeer>(loop, std::move(transport));
// Schedule stderr log collection
std::string prefix = "[" + worker_name + "]";
@@ -107,29 +107,24 @@ bool WorkerPool::spawn_worker(const std::string& self_path,
});
auto& w = workers.back();
w.alive = true;
++alive_count_;
loop.schedule(w.peer->run());
return true;
}
bool WorkerPool::start(const WorkerPoolOptions& options) {
options_ = options;
log_dir_ = options.log_dir;
for(std::uint32_t i = 0; i < options.stateless_count; ++i) {
if(!spawn_worker(options.self_path, false, 0)) {
return false;
}
loop.schedule(monitor_worker(stateless_workers.size() - 1, false));
}
for(std::uint32_t i = 0; i < options.stateful_count; ++i) {
if(!spawn_worker(options.self_path, true, options.worker_memory_limit)) {
return false;
}
loop.schedule(monitor_worker(stateful_workers.size() - 1, true));
}
// Register evicted notification handler for each stateful worker
@@ -147,26 +142,31 @@ bool WorkerPool::start(const WorkerPoolOptions& options) {
return true;
}
kota::task<> WorkerPool::stop() {
et::task<> WorkerPool::stop() {
LOG_INFO("WorkerPool stopping...");
shutting_down_ = true;
// Close output pipes to signal workers to exit gracefully.
for(auto& w: stateless_workers)
// Close output pipes to signal workers to exit gracefully
for(auto& w: stateless_workers) {
w.peer->close_output();
for(auto& w: stateful_workers)
}
for(auto& w: stateful_workers) {
w.peer->close_output();
}
// Send SIGTERM. monitor_worker coroutines handle the wait.
for(auto& w: stateless_workers)
// Send SIGTERM to all workers
for(auto& w: stateless_workers) {
w.proc.kill(SIGTERM);
for(auto& w: stateful_workers)
}
for(auto& w: stateful_workers) {
w.proc.kill(SIGTERM);
}
// Wait until all monitor_worker coroutines have finished.
if(alive_count_ > 0) {
all_exited_.reset();
co_await all_exited_.wait();
// Wait for all worker processes to exit
for(auto& w: stateless_workers) {
co_await w.proc.wait();
}
for(auto& w: stateful_workers) {
co_await w.proc.wait();
}
LOG_INFO("WorkerPool stopped");
@@ -197,10 +197,7 @@ std::size_t WorkerPool::assign_worker(std::uint32_t path_id) {
std::size_t WorkerPool::pick_least_loaded() {
std::size_t best = 0;
for(std::size_t i = 1; i < stateful_workers.size(); ++i) {
if(!stateful_workers[i].alive)
continue;
if(!stateful_workers[best].alive ||
stateful_workers[i].owned_documents < stateful_workers[best].owned_documents) {
if(stateful_workers[i].owned_documents < stateful_workers[best].owned_documents) {
best = i;
}
}
@@ -235,127 +232,4 @@ void WorkerPool::clear_owner(std::size_t worker_index) {
}
}
kota::task<> WorkerPool::monitor_worker(std::size_t index, bool stateful) {
auto& workers = stateful ? stateful_workers : stateless_workers;
auto& w = workers[index];
auto name = std::string(stateful ? "SF-" : "SL-") + std::to_string(index);
auto result = co_await w.proc.wait();
w.alive = false;
--alive_count_;
if(shutting_down_) {
if(alive_count_ == 0)
all_exited_.set();
co_return;
}
if(result.has_value()) {
auto& exit = result.value();
if(exit.term_signal != 0) {
LOG_ERROR("Worker {} killed by signal {} (restarts: {})",
name,
exit.term_signal,
w.restart_count);
} else {
LOG_ERROR("Worker {} exited with code {} (restarts: {})",
name,
exit.status,
w.restart_count);
}
} else {
LOG_ERROR("Worker {} lost: {} (restarts: {})",
name,
result.error().message(),
w.restart_count);
}
if(stateful)
clear_owner(index);
constexpr unsigned max_restarts = 5;
if(w.restart_count >= max_restarts) {
LOG_ERROR("Worker {} exceeded max restarts ({}), giving up", name, max_restarts);
co_return;
}
if(!respawn_worker(index, stateful)) {
LOG_ERROR("Worker {} respawn failed", name);
}
}
bool WorkerPool::respawn_worker(std::size_t index, bool stateful) {
auto& workers = stateful ? stateful_workers : stateless_workers;
auto old_restart_count = workers[index].restart_count + 1;
auto worker_name = std::string(stateful ? "SF-" : "SL-") + std::to_string(index);
// Close the old peer and retire it so its coroutines (run/write_loop)
// can finish naturally before the object is destroyed.
if(workers[index].peer) {
workers[index].peer->close();
retired_peers.push_back(std::move(workers[index].peer));
}
kota::process::options opts;
opts.file = options_.self_path;
if(stateful) {
opts.args = {options_.self_path,
"--mode",
"stateful-worker",
"--worker-memory-limit",
std::to_string(options_.worker_memory_limit)};
} else {
opts.args = {options_.self_path, "--mode", "stateless-worker"};
}
opts.args.push_back("--worker-name");
opts.args.push_back(worker_name);
if(!log_dir_.empty()) {
opts.args.push_back("--log-dir");
opts.args.push_back(log_dir_);
}
opts.streams = {
kota::process::stdio::pipe(true, false),
kota::process::stdio::pipe(false, true),
kota::process::stdio::pipe(false, true),
};
auto result = kota::process::spawn(opts, loop);
if(!result) {
LOG_ERROR("Failed to respawn worker {}: {}", worker_name, result.error().message());
return false;
}
auto& spawn = *result;
auto transport = std::make_unique<kota::ipc::StreamTransport>(std::move(spawn.stdout_pipe),
std::move(spawn.stdin_pipe));
auto peer = std::make_unique<kota::ipc::BincodePeer>(loop, std::move(transport));
std::string prefix = "[" + worker_name + "]";
loop.schedule(drain_stderr(std::move(spawn.stderr_pipe), prefix));
workers[index] = WorkerProcess{
.proc = std::move(spawn.proc),
.peer = std::move(peer),
.owned_documents = 0,
.alive = true,
.restart_count = old_restart_count,
};
auto& w = workers[index];
++alive_count_;
loop.schedule(w.peer->run());
if(stateful) {
w.peer->on_notification([this](const worker::EvictedParams& params) {
if(on_evicted)
on_evicted(params.path);
});
}
loop.schedule(monitor_worker(index, stateful));
LOG_INFO("Worker {} restarted (attempt {})", worker_name, old_restart_count);
return true;
}
} // namespace clice

View File

@@ -6,17 +6,17 @@
#include <list>
#include <memory>
#include "eventide/async/async.h"
#include "eventide/ipc/peer.h"
#include "server/protocol.h"
#include "kota/async/async.h"
#include "kota/ipc/codec/bincode.h"
#include "kota/ipc/peer.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/SmallVector.h"
namespace clice {
using kota::ipc::RequestResult;
namespace et = eventide;
using et::ipc::RequestResult;
struct WorkerPoolOptions {
std::string self_path;
@@ -28,24 +28,23 @@ struct WorkerPoolOptions {
class WorkerPool {
public:
WorkerPool(kota::event_loop& loop) : loop(loop) {}
WorkerPool(et::event_loop& loop) : loop(loop) {}
/// Spawn all worker processes. Returns false on failure.
bool start(const WorkerPoolOptions& options);
/// Gracefully stop all workers.
kota::task<> stop();
et::task<> stop();
/// Send a request to a stateful worker with path_id affinity routing.
template <typename Params>
RequestResult<Params> send_stateful(std::uint32_t path_id,
const Params& params,
kota::ipc::request_options opts = {});
et::ipc::request_options opts = {});
/// Send a request to a stateless worker with round-robin dispatch.
template <typename Params>
RequestResult<Params> send_stateless(const Params& params,
kota::ipc::request_options opts = {});
RequestResult<Params> send_stateless(const Params& params, et::ipc::request_options opts = {});
/// Send a notification to the stateful worker owning path_id (if any).
template <typename Params>
@@ -61,14 +60,12 @@ public:
private:
struct WorkerProcess {
kota::process proc;
std::unique_ptr<kota::ipc::BincodePeer> peer;
et::process proc;
std::unique_ptr<et::ipc::BincodePeer> peer;
std::size_t owned_documents = 0;
bool alive = true;
unsigned restart_count = 0;
};
kota::event_loop& loop;
et::event_loop& loop;
llvm::SmallVector<WorkerProcess> stateless_workers;
llvm::SmallVector<WorkerProcess> stateful_workers;
std::size_t next_stateless = 0;
@@ -82,51 +79,34 @@ private:
void clear_owner(std::size_t worker_index);
std::size_t pick_least_loaded();
bool shutting_down_ = false;
std::size_t alive_count_ = 0;
kota::event all_exited_{true}; // Signalled when alive_count_ reaches 0.
WorkerPoolOptions options_;
std::string log_dir_;
/// Peers moved here during respawn so their coroutines can finish
/// before the object is destroyed.
llvm::SmallVector<std::unique_ptr<kota::ipc::BincodePeer>> retired_peers;
bool spawn_worker(const std::string& self_path, bool stateful, std::uint64_t memory_limit);
bool respawn_worker(std::size_t index, bool stateful);
kota::task<> monitor_worker(std::size_t index, bool stateful);
};
template <typename Params>
RequestResult<Params> WorkerPool::send_stateful(std::uint32_t path_id,
const Params& params,
kota::ipc::request_options opts) {
et::ipc::request_options opts) {
if(stateful_workers.empty()) {
co_return kota::outcome_error(kota::ipc::Error{"No stateful workers available"});
co_return et::outcome_error(et::ipc::Error{"No stateful workers available"});
}
// No timeout: compile tasks run as detached tasks (loop.schedule) that
// are immune to LSP $/cancelRequest. Adding a timeout here would use
// eventide's with_token/when_any which has a spurious-cancellation bug
// that kills requests within milliseconds instead of the configured period.
auto idx = assign_worker(path_id);
if(!stateful_workers[idx].alive) {
co_return kota::outcome_error(kota::ipc::Error{"Assigned stateful worker is down"});
}
co_return co_await stateful_workers[idx].peer->send_request(params, opts);
}
template <typename Params>
RequestResult<Params> WorkerPool::send_stateless(const Params& params,
kota::ipc::request_options opts) {
et::ipc::request_options opts) {
if(stateless_workers.empty()) {
co_return kota::outcome_error(kota::ipc::Error{"No stateless workers available"});
co_return et::outcome_error(et::ipc::Error{"No stateless workers available"});
}
// Round-robin, skipping dead workers.
auto start = next_stateless;
for(std::size_t i = 0; i < stateless_workers.size(); ++i) {
auto idx = (start + i) % stateless_workers.size();
if(stateless_workers[idx].alive) {
next_stateless = (idx + 1) % stateless_workers.size();
co_return co_await stateless_workers[idx].peer->send_request(params, opts);
}
}
co_return kota::outcome_error(kota::ipc::Error{"All stateless workers are down"});
auto idx = next_stateless;
next_stateless = (next_stateless + 1) % stateless_workers.size();
co_return co_await stateless_workers[idx].peer->send_request(params, opts);
}
template <typename Params>
@@ -134,8 +114,6 @@ void WorkerPool::notify_stateful(std::uint32_t path_id, const Params& params) {
auto it = owner.find(path_id);
if(it == owner.end())
return;
if(!stateful_workers[it->second].alive)
return;
stateful_workers[it->second].peer->send_notification(params);
}

View File

@@ -3,13 +3,13 @@
#include <algorithm>
#include <chrono>
#include "eventide/ipc/lsp/position.h"
#include "eventide/ipc/lsp/protocol.h"
#include "eventide/serde/json/json.h"
#include "support/filesystem.h"
#include "support/logging.h"
#include "syntax/scan.h"
#include "kota/codec/json/json.h"
#include "kota/ipc/lsp/position.h"
#include "kota/ipc/lsp/protocol.h"
#include "llvm/Support/Chrono.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/MemoryBuffer.h"
@@ -18,7 +18,7 @@
namespace clice {
namespace lsp = kota::ipc::lsp;
namespace lsp = eventide::ipc::lsp;
/// Find the tightest (innermost) occurrence containing `offset` via binary search.
const static index::Occurrence* lookup_occurrence(const std::vector<index::Occurrence>& occs,
@@ -183,10 +183,10 @@ struct CacheData {
} // namespace
void Workspace::load_cache() {
if(config.project.cache_dir.empty())
if(config.cache_dir.empty())
return;
auto cache_path = path::join(config.project.cache_dir, "cache", "cache.json");
auto cache_path = path::join(config.cache_dir, "cache", "cache.json");
auto content = fs::read(cache_path);
if(!content) {
LOG_DEBUG("No cache.json found at {}", cache_path);
@@ -194,7 +194,7 @@ void Workspace::load_cache() {
}
CacheData data;
auto status = kota::codec::json::from_json(*content, data);
auto status = eventide::serde::json::from_json(*content, data);
if(!status) {
LOG_WARN("Failed to parse cache.json");
return;
@@ -218,7 +218,7 @@ void Workspace::load_cache() {
};
for(auto& entry: data.pch) {
auto pch_path = path::join(config.project.cache_dir, "cache", "pch", entry.filename);
auto pch_path = path::join(config.cache_dir, "cache", "pch", entry.filename);
auto source = resolve(entry.source_file);
if(!llvm::sys::fs::exists(pch_path) || source.empty())
continue;
@@ -234,7 +234,7 @@ void Workspace::load_cache() {
}
for(auto& entry: data.pcm) {
auto pcm_path = path::join(config.project.cache_dir, "cache", "pcm", entry.filename);
auto pcm_path = path::join(config.cache_dir, "cache", "pcm", entry.filename);
auto source = resolve(entry.source_file);
if(!llvm::sys::fs::exists(pcm_path) || source.empty())
continue;
@@ -252,7 +252,7 @@ void Workspace::load_cache() {
}
void Workspace::save_cache() {
if(config.project.cache_dir.empty())
if(config.cache_dir.empty())
return;
CacheData data;
@@ -300,13 +300,13 @@ void Workspace::save_cache() {
data.pcm.push_back(std::move(entry));
}
auto json_str = kota::codec::json::to_json(data);
auto json_str = eventide::serde::json::to_json(data);
if(!json_str) {
LOG_WARN("Failed to serialize cache.json");
return;
}
auto cache_path = path::join(config.project.cache_dir, "cache", "cache.json");
auto cache_path = path::join(config.cache_dir, "cache", "cache.json");
auto tmp_path = cache_path + ".tmp";
auto write_result = fs::write(tmp_path, *json_str);
if(!write_result) {
@@ -321,14 +321,14 @@ void Workspace::save_cache() {
}
void Workspace::cleanup_cache(int max_age_days) {
if(config.project.cache_dir.empty())
if(config.cache_dir.empty())
return;
auto now = std::chrono::system_clock::now();
auto max_age = std::chrono::hours(max_age_days * 24);
for(auto* subdir: {"cache/pch", "cache/pcm"}) {
auto dir = path::join(config.project.cache_dir, subdir);
auto dir = path::join(config.cache_dir, subdir);
std::error_code ec;
for(auto it = llvm::sys::fs::directory_iterator(dir, ec);
!ec && it != llvm::sys::fs::directory_iterator();

View File

@@ -8,6 +8,8 @@
#include <utility>
#include "command/command.h"
#include "eventide/ipc/lsp/position.h"
#include "eventide/ipc/lsp/protocol.h"
#include "index/merged_index.h"
#include "index/project_index.h"
#include "semantic/relation_kind.h"
@@ -16,8 +18,6 @@
#include "support/path_pool.h"
#include "syntax/dependency_graph.h"
#include "kota/ipc/lsp/position.h"
#include "kota/ipc/lsp/protocol.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/SmallVector.h"
@@ -25,8 +25,9 @@
namespace clice {
namespace protocol = kota::ipc::protocol;
namespace lsp = kota::ipc::lsp;
namespace et = eventide;
namespace protocol = et::ipc::protocol;
namespace lsp = et::ipc::lsp;
/// Two-layer staleness snapshot for compilation artifacts (PCH, AST, etc.).
///
@@ -140,7 +141,7 @@ struct PCHState {
std::uint64_t hash = 0;
DepsSnapshot deps;
std::string document_links_json; ///< Pre-serialized DocumentLink[] from PCH build
std::shared_ptr<kota::event> building;
std::shared_ptr<eventide::event> building;
};
/// Cached PCM state for a single C++20 module. Shared across all files that
@@ -170,7 +171,7 @@ struct PCMState {
/// - didSave (on_file_saved: rescan disk, cascade invalidation)
/// - Background index (merge TUIndex results from stateless workers)
struct Workspace {
Config config;
CliceConfig config;
CompilationDatabase cdb;
PathPool path_pool;

View File

@@ -6,10 +6,11 @@
#include <system_error>
#include <type_traits>
#include "kota/meta/enum.h"
#include "kota/meta/struct.h"
#include "kota/support/ranges.h"
#include "kota/support/type_traits.h"
#include "eventide/common/meta.h"
#include "eventide/common/ranges.h"
#include "eventide/reflection/enum.h"
#include "eventide/reflection/struct.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/Error.h"
@@ -85,7 +86,7 @@ struct std::formatter<std::error_code> : std::formatter<std::string_view> {
}
};
template <kota::meta::enum_type E>
template <eventide::refl::enum_type E>
struct std::formatter<E> : std::formatter<std::string> {
using Base = std::formatter<std::string>;
@@ -96,7 +97,7 @@ struct std::formatter<E> : std::formatter<std::string> {
template <typename FormatContext>
auto format(const E& value, FormatContext& ctx) const {
auto name = kota::meta::enum_name(value);
auto name = eventide::refl::enum_name(value);
if(name.empty()) {
using U = std::underlying_type_t<E>;
return Base::format(std::format("{}", static_cast<U>(value)), ctx);
@@ -106,8 +107,9 @@ struct std::formatter<E> : std::formatter<std::string> {
};
template <typename T>
concept clice_reflectable_class = kota::meta::reflectable_class<T> && !kota::sequence_range<T> &&
!kota::set_range<T> && !kota::map_range<T>;
concept clice_reflectable_class =
eventide::refl::reflectable_class<T> && !eventide::sequence_range<T> &&
!eventide::set_range<T> && !eventide::map_range<T>;
template <clice_reflectable_class T>
struct std::formatter<T> : std::formatter<std::string> {
@@ -136,7 +138,7 @@ std::string dump(const Object& object) {
return std::format("\"{}\"", object);
} else if constexpr(std::is_same_v<T, llvm::StringRef>) {
return std::format("\"{}\"", object);
} else if constexpr(kota::map_range<T>) {
} else if constexpr(eventide::map_range<T>) {
std::string result = "{";
bool first = true;
for(auto&& [key, value]: object) {
@@ -148,8 +150,8 @@ std::string dump(const Object& object) {
}
result += "}";
return result;
} else if constexpr(kota::set_range<T> || kota::sequence_range<T>) {
std::string result = kota::set_range<T> ? "{" : "[";
} else if constexpr(eventide::set_range<T> || eventide::sequence_range<T>) {
std::string result = eventide::set_range<T> ? "{" : "[";
bool first = true;
for(auto&& value: object) {
if(!first) {
@@ -158,10 +160,10 @@ std::string dump(const Object& object) {
first = false;
result += dump(value);
}
result += kota::set_range<T> ? "}" : "]";
result += eventide::set_range<T> ? "}" : "]";
return result;
} else if constexpr(kota::meta::enum_type<T>) {
auto name = kota::meta::enum_name(object);
} else if constexpr(eventide::refl::enum_type<T>) {
auto name = eventide::refl::enum_name(object);
if(!name.empty()) {
return std::format("\"{}\"", name);
}
@@ -170,7 +172,7 @@ std::string dump(const Object& object) {
} else if constexpr(clice_reflectable_class<T>) {
std::string result = "{";
bool first = true;
kota::meta::for_each(object, [&](auto field) {
eventide::refl::for_each(object, [&](auto field) {
if(!first) {
result += ", ";
}
@@ -179,7 +181,7 @@ std::string dump(const Object& object) {
});
result += "}";
return result;
} else if constexpr(kota::Formattable<T>) {
} else if constexpr(eventide::Formattable<T>) {
return std::format("{}", object);
} else {
return "<unformattable>";

View File

@@ -289,7 +289,7 @@ std::expected<GlobPattern::SubGlobPattern, std::string>
return pat;
}
bool GlobPattern::match(llvm::StringRef str) const {
bool GlobPattern::match(llvm::StringRef str) {
if(!str.consume_front(prefix)) {
return false;
}

View File

@@ -54,7 +54,7 @@ public:
}
/// \returns \p true if \p str matches this glob pattern
bool match(llvm::StringRef s) const;
bool match(llvm::StringRef s);
private:
/// GlobPattern is seperated into `Prefix + SubGlobPattern`

View File

@@ -4,11 +4,11 @@
#include <chrono>
#include "command/toolchain.h"
#include "eventide/async/async.h"
#include "support/logging.h"
#include "syntax/include_resolver.h"
#include "syntax/scan.h"
#include "kota/async/async.h"
#include "llvm/ADT/DenseSet.h"
#include "llvm/ADT/StringSet.h"
#include "llvm/Support/FileSystem.h"
@@ -18,6 +18,8 @@
namespace clice {
namespace et = eventide;
// DependencyGraph implementation
void DependencyGraph::add_module(llvm::StringRef module_name, std::uint32_t path_id) {
@@ -251,13 +253,12 @@ FileScanResult scan_file_worker(const char* path, std::uint32_t path_id, std::ui
}
/// The async scan implementation that runs on a local event loop.
kota::task<> scan_impl(CompilationDatabase& cdb,
PathPool& path_pool,
DependencyGraph& graph,
ScanReport& report,
ScanCache* ext_cache,
kota::event_loop& loop,
const RuleMatcher& rule_matcher) {
et::task<> scan_impl(CompilationDatabase& cdb,
PathPool& path_pool,
DependencyGraph& graph,
ScanReport& report,
ScanCache* ext_cache,
et::event_loop& loop) {
auto start_time = std::chrono::steady_clock::now();
// Reuse context groups and configs from cache when available (warm runs).
@@ -315,10 +316,10 @@ kota::task<> scan_impl(CompilationDatabase& cdb,
if(!pending.empty()) {
LOG_INFO("Warming toolchain cache: {} unique queries", pending.size());
std::vector<kota::task<ToolchainResult, kota::error>> tasks;
std::vector<et::task<ToolchainResult, et::error>> tasks;
tasks.reserve(pending.size());
for(auto& query: pending) {
tasks.push_back(kota::queue(
tasks.push_back(et::queue(
[q = std::move(query)]() -> ToolchainResult {
ToolchainResult result;
result.key = q.key;
@@ -336,7 +337,7 @@ kota::task<> scan_impl(CompilationDatabase& cdb,
loop));
}
auto outcome = co_await kota::when_all(std::move(tasks));
auto outcome = co_await et::when_all(std::move(tasks));
if(outcome.has_value()) {
cdb.inject_results(*outcome);
} else {
@@ -356,19 +357,9 @@ kota::task<> scan_impl(CompilationDatabase& cdb,
std::uint32_t config_id = next_config_id++;
context_to_config_id[context] = config_id;
auto representative_path = path_pool.resolve(file_ids[0]);
// Apply per-file rules so that `[[rules]]`-modified -I/-isystem/-std
// flags are reflected in the search config used by the scan.
// Rules are applied to the representative file and assumed to hold
// for the whole context group (same CompilationInfo).
std::vector<std::string> rule_append, rule_remove;
if(rule_matcher)
rule_matcher(representative_path, rule_append, rule_remove);
auto t0 = std::chrono::steady_clock::now();
configs[config_id] = cdb.lookup_search_config(
representative_path,
{.query_toolchain = true, .remove = rule_remove, .append = rule_append});
configs[config_id] =
cdb.lookup_search_config(representative_path, {.query_toolchain = true});
auto t1 = std::chrono::steady_clock::now();
lookup_us += std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
}
@@ -399,7 +390,7 @@ kota::task<> scan_impl(CompilationDatabase& cdb,
llvm::StringSet<> entries;
};
std::vector<kota::task<DirEntry, kota::error>> pending_dir_tasks;
std::vector<et::task<DirEntry, et::error>> pending_dir_tasks;
if(dir_cache.dirs.empty()) {
llvm::StringSet<> unique_dirs;
@@ -421,7 +412,7 @@ kota::task<> scan_impl(CompilationDatabase& cdb,
pending_dir_tasks.reserve(unique_dirs.size());
for(auto& entry: unique_dirs) {
auto dir_path = entry.getKey().str();
pending_dir_tasks.push_back(kota::queue(
pending_dir_tasks.push_back(et::queue(
[dir_path = std::move(dir_path)]() -> DirEntry {
DirEntry result;
result.dir_path = dir_path;
@@ -472,7 +463,7 @@ kota::task<> scan_impl(CompilationDatabase& cdb,
// queued for scanning on the thread pool. When wave N+1 starts,
// these tasks are already running (or finished), eliminating most
// of the Phase 1 wait time for subsequent waves.
std::vector<kota::task<FileScanResult, kota::error>> prefetch_tasks;
std::vector<et::task<FileScanResult, et::error>> prefetch_tasks;
// Pre-resolved search configs: built once after dir cache is populated,
// then reused for all waves. Eliminates StringMap lookups in Phase 2.
@@ -509,7 +500,7 @@ kota::task<> scan_impl(CompilationDatabase& cdb,
if(!prefetch_tasks.empty()) {
// Waves 1+: await prefetched scan tasks from previous Phase 2.
auto scan_outcome = co_await kota::when_all(std::move(prefetch_tasks));
auto scan_outcome = co_await et::when_all(std::move(prefetch_tasks));
prefetch_tasks.clear();
if(scan_outcome.has_error()) {
LOG_ERROR("Prefetch scan failed: {}", scan_outcome.error().message());
@@ -523,7 +514,7 @@ kota::task<> scan_impl(CompilationDatabase& cdb,
}
} else {
// Wave 0 (or warm run with all cache hits): create scan tasks now.
std::vector<kota::task<FileScanResult, kota::error>> scan_tasks;
std::vector<et::task<FileScanResult, et::error>> scan_tasks;
scan_tasks.reserve(current_wave.size());
for(auto& entry: current_wave) {
auto pid = entry.path_id;
@@ -534,8 +525,8 @@ kota::task<> scan_impl(CompilationDatabase& cdb,
}
auto path = path_pool.resolve(pid).data();
scan_tasks.push_back(
kota::queue([path, pid, cid]() { return scan_file_worker(path, pid, cid); },
loop));
et::queue([path, pid, cid]() { return scan_file_worker(path, pid, cid); },
loop));
}
// Optimization 1: await dir cache tasks concurrently with scan tasks.
@@ -544,7 +535,7 @@ kota::task<> scan_impl(CompilationDatabase& cdb,
// max(dir_time, scan_time) instead of dir_time + scan_time.
if(!pending_dir_tasks.empty()) {
auto dir_t0 = std::chrono::steady_clock::now();
auto dir_outcome = co_await kota::when_all(std::move(pending_dir_tasks));
auto dir_outcome = co_await et::when_all(std::move(pending_dir_tasks));
pending_dir_tasks.clear();
if(dir_outcome.has_value()) {
for(auto& entry: *dir_outcome) {
@@ -558,7 +549,7 @@ kota::task<> scan_impl(CompilationDatabase& cdb,
}
if(!scan_tasks.empty()) {
auto scan_outcome = co_await kota::when_all(std::move(scan_tasks));
auto scan_outcome = co_await et::when_all(std::move(scan_tasks));
if(scan_outcome.has_error()) {
LOG_ERROR("Parallel scan failed: {}", scan_outcome.error().message());
break;
@@ -758,7 +749,7 @@ kota::task<> scan_impl(CompilationDatabase& cdb,
if(!ext_cache ||
ext_cache->scan_results.find(inc_path_id) == ext_cache->scan_results.end()) {
auto inc_path = path_pool.resolve(inc_path_id).data();
prefetch_tasks.push_back(kota::queue(
prefetch_tasks.push_back(et::queue(
[inc_path, inc_path_id, cid = scan_result.config_id]() {
return scan_file_worker(inc_path, inc_path_id, cid);
},
@@ -830,15 +821,14 @@ kota::task<> scan_impl(CompilationDatabase& cdb,
ScanReport scan_dependency_graph(CompilationDatabase& cdb,
PathPool& path_pool,
DependencyGraph& graph,
ScanCache* cache,
const RuleMatcher& rule_matcher) {
ScanCache* cache) {
ScanReport report;
if(cdb.get_entries().empty()) {
return report;
}
kota::event_loop loop;
loop.schedule(scan_impl(cdb, path_pool, graph, report, cache, loop, rule_matcher));
et::event_loop loop;
loop.schedule(scan_impl(cdb, path_pool, graph, report, cache, loop));
loop.run();
return report;
}

View File

@@ -1,7 +1,6 @@
#pragma once
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
@@ -254,12 +253,6 @@ struct ScanCache {
std::vector<WaveEntry> initial_wave;
};
/// Callback for per-file rule-based flag modification. Given a file path,
/// populates `append`/`remove` with rule-configured arguments so they can be
/// layered on top of the CDB command when extracting the search config.
using RuleMatcher = std::function<
void(llvm::StringRef path, std::vector<std::string>& append, std::vector<std::string>& remove)>;
/// Run the wavefront BFS scan over all files in the compilation database.
/// Internally creates a local event loop for async I/O (file reads via worker
/// thread pool, stat calls via libuv). Blocks until the scan is complete.
@@ -268,14 +261,9 @@ using RuleMatcher = std::function<
/// avoids repeated readdir() and include-resolution work across
/// successive calls. PathPool must NOT be reset between calls
/// when a persistent cache is used (path_id values must remain stable).
/// @param rule_matcher Optional callback applied per context group so that
/// `[[rules]]`-modified include/std flags are reflected in the
/// dependency graph (otherwise rule-affected files would have
/// stale resolution).
ScanReport scan_dependency_graph(CompilationDatabase& cdb,
PathPool& path_pool,
DependencyGraph& graph,
ScanCache* cache = nullptr,
const RuleMatcher& rule_matcher = {});
ScanCache* cache = nullptr);
} // namespace clice

View File

@@ -106,60 +106,4 @@ Token Lexer::advance_until(TokenKind kind) {
}
}
static bool is_directive_keyword(llvm::StringRef word) {
return word == "include" || word == "include_next" || word == "import" || word == "embed" ||
word == "__has_include" || word == "__has_include_next" || word == "__has_embed";
}
std::optional<LocalSourceRange> find_directive_argument(llvm::StringRef content,
std::uint32_t offset,
const clang::LangOptions* lang_opts) {
std::uint32_t line_start = 0;
if(auto nl = content.rfind('\n', offset); nl != llvm::StringRef::npos)
line_start = static_cast<std::uint32_t>(nl + 1);
auto line = content.substr(line_start);
Lexer lexer(line, true, lang_opts);
bool after_has_keyword = false;
bool ready = false;
while(true) {
auto tok = lexer.advance();
if(tok.is_eof() || tok.is_eod())
break;
auto abs_begin = line_start + tok.range.begin;
auto abs_end = line_start + tok.range.end;
if(tok.is_identifier()) {
auto text = tok.text(line);
if(text == "__has_include" || text == "__has_include_next" || text == "__has_embed") {
after_has_keyword = true;
continue;
}
if(text == "include" || text == "include_next" || text == "embed") {
ready = true;
continue;
}
}
if(tok.kind == clang::tok::l_paren && after_has_keyword) {
after_has_keyword = false;
ready = true;
lexer.set_header_name_mode();
continue;
}
if(abs_begin < offset || !ready)
continue;
if(tok.is_header_name() || tok.kind == clang::tok::string_literal)
return LocalSourceRange(abs_begin, abs_end);
if(tok.is_identifier())
return LocalSourceRange(abs_begin, abs_end);
}
return std::nullopt;
}
} // namespace clice

View File

@@ -73,13 +73,4 @@ private:
std::unique_ptr<clang::Lexer> lexer;
};
/// Find the range of the filename argument in a preprocessor directive line.
/// `content` is the full source text, `offset` points at or before the directive keyword.
/// Returns the range of the first filename-like token (header name, string literal,
/// or macro identifier) found on the same line, or nullopt if none.
std::optional<LocalSourceRange>
find_directive_argument(llvm::StringRef content,
std::uint32_t offset,
const clang::LangOptions* lang_opts = nullptr);
} // namespace clice

View File

@@ -10,14 +10,6 @@ import pytest
from tests.integration.utils.client import CliceClient
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""Store test outcome so fixtures can detect failures."""
outcome = yield
rep = outcome.get_result()
setattr(item, f"rep_{rep.when}", rep)
def pytest_addoption(parser: pytest.Parser) -> None:
parser.addoption(
"--executable",
@@ -83,8 +75,7 @@ def workspace(request: pytest.FixtureRequest, test_data_dir: Path) -> Path | Non
"""
marker = request.node.get_closest_marker("workspace")
if marker is None:
yield None
return
return None
if not marker.args or not isinstance(marker.args[0], str):
raise pytest.UsageError(
"@pytest.mark.workspace requires a string argument, e.g. "
@@ -97,10 +88,7 @@ def workspace(request: pytest.FixtureRequest, test_data_dir: Path) -> Path | Non
clice_dir = path / ".clice"
if clice_dir.exists():
shutil.rmtree(clice_dir)
yield path
# Post-test cleanup: remove cache generated during the test.
if clice_dir.exists():
shutil.rmtree(clice_dir)
return path
@pytest.fixture
@@ -121,21 +109,11 @@ async def client(
await c.start_io(*cmd)
if workspace is not None:
init_options_marker = request.node.get_closest_marker("init_options")
init_options = dict(init_options_marker.args[0]) if init_options_marker else {}
# Force cache_dir into the workspace so .clice/ cleanup prevents stale PCH.
project = dict(init_options.get("project", {}))
project.setdefault("cache_dir", str(workspace / ".clice"))
init_options["project"] = project
await c.initialize(workspace, initialization_options=init_options)
await c.initialize(workspace)
yield c
test_failed = (
getattr(request.node, "rep_call", None) is not None
and request.node.rep_call.failed
)
await _shutdown_client(c, verbose=test_failed)
await _shutdown_client(c)
def generate_cdb(workspace: Path) -> None:
@@ -168,12 +146,8 @@ async def make_client(executable: Path, workspace: Path) -> CliceClient:
return c
async def _shutdown_client(c: CliceClient, *, verbose: bool = False) -> None:
"""Gracefully shut down a client, force-kill if needed.
When verbose=True (typically on test failure), dump collected log messages
and server stderr to help diagnose the failure.
"""
async def _shutdown_client(c: CliceClient) -> None:
"""Gracefully shut down a client, force-kill if needed."""
try:
await asyncio.wait_for(c.shutdown_async(None), timeout=3.0)
except Exception:
@@ -189,25 +163,15 @@ async def _shutdown_client(c: CliceClient, *, verbose: bool = False) -> None:
try:
server = getattr(c, "_server", None)
if server:
if server.returncode is not None:
print(f"[server] exit code: {server.returncode}", flush=True)
if server.stderr:
stderr_data = await asyncio.wait_for(server.stderr.read(), timeout=2.0)
if stderr_data:
for line in stderr_data.decode(
"utf-8", errors="replace"
).splitlines():
if "[warn]" in line or "[error]" in line or "Sanitizer" in line:
print(f"[server] {line}", flush=True)
if server and server.stderr:
stderr_data = await asyncio.wait_for(server.stderr.read(), timeout=2.0)
if stderr_data:
for line in stderr_data.decode("utf-8", errors="replace").splitlines():
if "[warn]" in line or "[error]" in line:
print(f"[server] {line}", flush=True)
except Exception:
pass
if verbose and c.log_messages:
for msg in c.log_messages:
level = {1: "ERROR", 2: "WARN", 3: "INFO", 4: "LOG"}.get(msg.type, "?")
print(f"[logMessage/{level}] {msg.message}", flush=True)
try:
c._stop_event.set()
for task in c._async_tasks:
@@ -275,15 +239,6 @@ def _generate_test_data_cdbs(data_dir: Path) -> None:
dl_dir, [_entry(dl_dir, dl_main, [f"-I{dl_dir.as_posix()}", "-std=c++23"])]
)
# config_rules_toml / config_rules_no_config — rules tests must start
# from a CDB that does NOT include the flag the rule will append, so the
# rule's effect is observable through diagnostics.
for name in ("config_rules_toml", "config_rules_no_config"):
cr_dir = data_dir / name
cr_main = cr_dir / "main.cpp"
if cr_main.exists():
_write(cr_dir, [_entry(cr_dir, cr_main)])
# pch_test
pt_dir = data_dir / "pch_test"
if pt_dir.exists():

View File

@@ -1,7 +0,0 @@
int value() {
return FROM_INIT;
}
int main() {
return value();
}

View File

@@ -1,3 +0,0 @@
[[rules]]
patterns = ["**/*.cpp"]
append = ["-DFROM_TOML"]

View File

@@ -1,7 +0,0 @@
int value() {
return FROM_TOML;
}
int main() {
return value();
}

View File

@@ -16,7 +16,6 @@ from lsprotocol.types import (
from tests.conftest import make_client, shutdown_client
from tests.integration.utils import write_cdb, doc
from tests.integration.utils.wait import MTIME_GRANULARITY, SETTLE_TIME
from tests.integration.utils.cache import (
list_pch_files,
list_pcm_files,
@@ -25,17 +24,9 @@ from tests.integration.utils.cache import (
from tests.integration.utils.assertions import assert_clean_compile
def _pin_cache_to_workspace(tmp_path):
"""Write a clice.toml that pins cache_dir to <workspace>/.clice/."""
(tmp_path / "clice.toml").write_text(
'[project]\ncache_dir = "${workspace}/.clice"\n'
)
async def test_pch_written_to_cache_dir(client, tmp_path):
"""After opening a file with #include, a .pch file should appear
in .clice/cache/pch/ with a hex-hash filename."""
_pin_cache_to_workspace(tmp_path)
(tmp_path / "header.h").write_text("#pragma once\nstruct Foo { int x; };\n")
(tmp_path / "main.cpp").write_text(
'#include "header.h"\nint main() { Foo f; return f.x; }\n'
@@ -57,7 +48,6 @@ async def test_pch_written_to_cache_dir(client, tmp_path):
async def test_cache_json_persisted(client, tmp_path):
"""After a PCH build, cache.json should be written with the entry."""
_pin_cache_to_workspace(tmp_path)
(tmp_path / "header.h").write_text("#pragma once\nint global_val = 42;\n")
(tmp_path / "main.cpp").write_text(
'#include "header.h"\nint main() { return global_val; }\n'
@@ -84,7 +74,6 @@ async def test_cache_json_persisted(client, tmp_path):
async def test_pch_reused_on_close_reopen(client, tmp_path):
"""Closing and reopening a file within the same session should reuse
the cached PCH — no additional .pch files should be created."""
_pin_cache_to_workspace(tmp_path)
(tmp_path / "header.h").write_text("#pragma once\nstruct Bar { int y; };\n")
(tmp_path / "main.cpp").write_text(
'#include "header.h"\nint main() { Bar b; return b.y; }\n'
@@ -101,7 +90,7 @@ async def test_pch_reused_on_close_reopen(client, tmp_path):
# Close.
client.text_document_did_close(DidCloseTextDocumentParams(text_document=doc(uri)))
await asyncio.sleep(SETTLE_TIME)
await asyncio.sleep(0.5)
# Clear diagnostics so we can wait for fresh ones.
client.diagnostics.pop(uri, None)
@@ -119,7 +108,6 @@ async def test_pch_reused_on_close_reopen(client, tmp_path):
async def test_pch_survives_server_restart(executable, tmp_path):
"""PCH cache should survive a full server restart — cache.json is
loaded on startup and the existing .pch file is reused."""
_pin_cache_to_workspace(tmp_path)
(tmp_path / "header.h").write_text("#pragma once\nstruct Baz { int z; };\n")
(tmp_path / "main.cpp").write_text(
'#include "header.h"\nint main() { Baz b; return b.z; }\n'
@@ -162,7 +150,6 @@ async def test_pch_survives_server_restart(executable, tmp_path):
async def test_shared_preamble_shares_pch(client, tmp_path):
"""Two files with identical preambles should share the same PCH file
(content-addressed by preamble hash)."""
_pin_cache_to_workspace(tmp_path)
(tmp_path / "header.h").write_text("#pragma once\nint shared_val = 1;\n")
(tmp_path / "a.cpp").write_text(
'#include "header.h"\nint fa() { return shared_val; }\n'
@@ -189,7 +176,6 @@ async def test_shared_preamble_shares_pch(client, tmp_path):
async def test_different_preamble_different_pch(client, tmp_path):
"""Files with different preambles should produce different PCH files."""
_pin_cache_to_workspace(tmp_path)
(tmp_path / "a.h").write_text("#pragma once\nint val_a = 1;\n")
(tmp_path / "b.h").write_text("#pragma once\nint val_b = 2;\n")
(tmp_path / "a.cpp").write_text('#include "a.h"\nint fa() { return val_a; }\n')
@@ -213,7 +199,6 @@ async def test_different_preamble_different_pch(client, tmp_path):
async def test_pch_rebuilt_on_header_change(client, tmp_path):
"""When a preamble header changes, a new PCH should be built
(different hash → different filename). The old one remains for cleanup."""
_pin_cache_to_workspace(tmp_path)
(tmp_path / "header.h").write_text("#pragma once\nstruct V1 { int a; };\n")
(tmp_path / "main.cpp").write_text(
'#include "header.h"\nint main() { V1 v; return v.a; }\n'
@@ -228,7 +213,7 @@ async def test_pch_rebuilt_on_header_change(client, tmp_path):
assert len(pch_before) >= 1
# Modify header — changes preamble content hash.
await asyncio.sleep(MTIME_GRANULARITY)
await asyncio.sleep(1.1)
(tmp_path / "header.h").write_text("#pragma once\nstruct V2 { int b; };\n")
# Also update main.cpp to use V2 so it compiles cleanly.
(tmp_path / "main.cpp").write_text(
@@ -237,7 +222,7 @@ async def test_pch_rebuilt_on_header_change(client, tmp_path):
# Close and reopen to get fresh preamble.
client.text_document_did_close(DidCloseTextDocumentParams(text_document=doc(uri)))
await asyncio.sleep(SETTLE_TIME)
await asyncio.sleep(0.5)
client.diagnostics.pop(uri, None)
uri2, _ = await client.open_and_wait(tmp_path / "main.cpp")
@@ -255,7 +240,6 @@ async def test_pch_rebuilt_on_header_change(client, tmp_path):
async def test_no_tmp_files_after_build(client, tmp_path):
"""After a successful PCH build, no .tmp files should remain in the cache dir."""
_pin_cache_to_workspace(tmp_path)
(tmp_path / "header.h").write_text("#pragma once\nint val = 1;\n")
(tmp_path / "main.cpp").write_text(
'#include "header.h"\nint main() { return val; }\n'
@@ -281,7 +265,6 @@ async def test_no_tmp_files_after_build(client, tmp_path):
async def test_cache_dirs_created_on_startup(client, tmp_path):
"""The .clice/cache/pch/ and .clice/cache/pcm/ directories should be created
when the server initializes a workspace."""
_pin_cache_to_workspace(tmp_path)
(tmp_path / "main.cpp").write_text("int main() { return 0; }\n")
write_cdb(tmp_path, ["main.cpp"])
await client.initialize(tmp_path)

View File

@@ -21,7 +21,7 @@ from lsprotocol.types import (
)
from tests.integration.utils import write_cdb, doc
from tests.integration.utils.wait import MTIME_GRANULARITY, wait_for_recompile
from tests.integration.utils.wait import wait_for_recompile
from tests.integration.utils.assertions import assert_clean_compile, assert_has_errors
@@ -42,7 +42,7 @@ async def test_header_change_invalidates_ast(client, tmp_path):
# Modify header on disk — introduce an error.
# Ensure mtime advances past filesystem granularity (1s on some FSes).
await asyncio.sleep(MTIME_GRANULARITY)
await asyncio.sleep(1.1)
(tmp_path / "header.h").write_text(
"inline int value() { return }\n"
) # syntax error
@@ -71,7 +71,7 @@ async def test_header_change_invalidates_pch(client, tmp_path):
# Modify header — rename struct field.
# Ensure mtime advances past filesystem granularity (1s on some FSes).
await asyncio.sleep(MTIME_GRANULARITY)
await asyncio.sleep(1.1)
(tmp_path / "header.h").write_text(
"#pragma once\nstruct Foo { int y; };\n" # x -> y
)
@@ -115,22 +115,16 @@ async def test_touch_without_content_change_skips_recompile(client, tmp_path):
assert_clean_compile(client, uri)
# Touch the header — mtime changes but content stays the same.
await asyncio.sleep(MTIME_GRANULARITY)
await asyncio.sleep(1.1)
original_content = (tmp_path / "header.h").read_text()
(tmp_path / "header.h").write_text(original_content)
# Hover triggers ensure_compiled which runs deps_changed.
# Layer 2 hash confirms nothing actually changed → cached AST reused.
# The first hover may see ast_dirty=true (mtime changed, hash check in progress),
# so retry to let the hash check complete.
hover = None
for _ in range(3):
hover = await client.text_document_hover_async(
HoverParams(text_document=doc(uri), position=Position(line=1, character=4))
)
if hover is not None:
break
await asyncio.sleep(SETTLE_TIME)
# Hover on "main" (line 1, col 4) which should be hoverable.
hover = await client.text_document_hover_async(
HoverParams(text_document=doc(uri), position=Position(line=1, character=4))
)
assert hover is not None
# No new diagnostics should appear — the file is still clean.
@@ -151,7 +145,7 @@ async def test_header_replaced_with_different_content(client, tmp_path):
assert_clean_compile(client, uri)
# Replace header — delete and recreate with a breaking change.
await asyncio.sleep(MTIME_GRANULARITY)
await asyncio.sleep(1.1)
(tmp_path / "header.h").unlink()
(tmp_path / "header.h").write_text("inline int renamed_value() { return 1; }\n")
@@ -176,7 +170,7 @@ async def test_fix_error_clears_diagnostics(client, tmp_path):
assert_has_errors(client, uri, "Expected diagnostics from broken header")
# Fix the header.
await asyncio.sleep(MTIME_GRANULARITY)
await asyncio.sleep(1.1)
(tmp_path / "header.h").write_text("inline int value() { return 1; }\n")
# Hover triggers recompilation — diagnostics should clear.
@@ -204,7 +198,7 @@ async def test_multiple_files_share_header(client, tmp_path):
assert_clean_compile(client, uri_b)
# Break the shared header.
await asyncio.sleep(MTIME_GRANULARITY)
await asyncio.sleep(1.1)
(tmp_path / "shared.h").write_text("inline int shared() { return }\n")
# Both files should get diagnostics after hover.
@@ -229,7 +223,7 @@ async def test_transitive_header_change(client, tmp_path):
assert_clean_compile(client, uri)
# Modify the transitive dep (base.h).
await asyncio.sleep(MTIME_GRANULARITY)
await asyncio.sleep(1.1)
(tmp_path / "base.h").write_text("inline int base() { return }\n") # broken
await wait_for_recompile(client, uri)
@@ -316,7 +310,7 @@ async def test_didclose_then_reopen(client, tmp_path):
client.text_document_did_close(DidCloseTextDocumentParams(text_document=doc(uri)))
# Modify on disk while closed.
await asyncio.sleep(MTIME_GRANULARITY)
await asyncio.sleep(1.1)
(tmp_path / "main.cpp").write_text("int main() { return }\n") # broken
# Reopen — should compile the new (broken) content from disk.
@@ -327,7 +321,7 @@ async def test_didclose_then_reopen(client, tmp_path):
async def test_didclose_clears_hover(client, tmp_path):
"""After didClose, hover on the closed file should return an error."""
"""After didClose, hover on the closed file should return None."""
(tmp_path / "main.cpp").write_text("int main() { return 0; }\n")
write_cdb(tmp_path, ["main.cpp"])
await client.initialize(tmp_path)
@@ -336,10 +330,10 @@ async def test_didclose_clears_hover(client, tmp_path):
client.text_document_did_close(DidCloseTextDocumentParams(text_document=doc(uri)))
with pytest.raises(Exception, match="Document not open"):
await client.text_document_hover_async(
HoverParams(text_document=doc(uri), position=Position(line=0, character=4))
)
hover = await client.text_document_hover_async(
HoverParams(text_document=doc(uri), position=Position(line=0, character=4))
)
assert hover is None, "Hover on closed file should return None"
async def test_didsave_triggers_recompile_for_dependents(client, tmp_path):
@@ -355,7 +349,7 @@ async def test_didsave_triggers_recompile_for_dependents(client, tmp_path):
assert_clean_compile(client, uri)
# Modify header on disk and send didSave.
await asyncio.sleep(MTIME_GRANULARITY)
await asyncio.sleep(1.1)
(tmp_path / "header.h").write_text("inline int value() { return }\n") # broken
client.text_document_did_save(
DidSaveTextDocumentParams(

View File

@@ -10,7 +10,6 @@ from lsprotocol.types import (
)
from tests.integration.utils import doc
from tests.integration.utils.wait import SETTLE_TIME
from tests.integration.utils.workspace import did_change
@@ -71,7 +70,7 @@ async def test_semantic_token_modifier_legend(client, workspace):
@pytest.mark.workspace("hello_world")
async def test_did_open_close_cycle(client, workspace):
uri, _ = client.open(workspace / "main.cpp")
await asyncio.sleep(SETTLE_TIME)
await asyncio.sleep(0.5)
client.close(uri)
@@ -84,8 +83,8 @@ async def test_shutdown_exit(client, workspace):
async def test_feature_requests_after_close(client, workspace):
uri, _ = client.open(workspace / "main.cpp")
client.close(uri)
with pytest.raises(Exception, match="Document not open"):
await client.hover_at(uri, 0, 0)
result = await client.hover_at(uri, 0, 0)
assert result is None
@pytest.mark.workspace("hello_world")
@@ -95,7 +94,7 @@ async def test_incremental_change(client, workspace):
content += f"\n// change {i}"
did_change(client, uri, i + 1, content)
await asyncio.sleep(0.05)
await asyncio.sleep(SETTLE_TIME * 2)
await asyncio.sleep(1)
client.close(uri)
@@ -192,23 +191,23 @@ async def test_rapid_changes_stress(client, workspace):
for i in range(20):
content += f"\n// stress change {i}\n"
did_change(client, uri, i + 1, content)
await asyncio.sleep(SETTLE_TIME * 2)
await asyncio.sleep(2)
client.close(uri)
@pytest.mark.workspace("hello_world")
async def test_save_notification(client, workspace):
uri, _ = client.open(workspace / "main.cpp")
await asyncio.sleep(SETTLE_TIME)
await asyncio.sleep(0.5)
client.text_document_did_save(DidSaveTextDocumentParams(text_document=doc(uri)))
await asyncio.sleep(SETTLE_TIME)
await asyncio.sleep(0.5)
client.close(uri)
@pytest.mark.workspace("hello_world")
async def test_hover_on_unknown_file(client, workspace):
with pytest.raises(Exception, match="Document not open"):
await client.hover_at("file:///nonexistent/fake.cpp", 0, 0)
result = await client.hover_at("file:///nonexistent/fake.cpp", 0, 0)
assert result is None
@pytest.mark.workspace("hello_world")

View File

@@ -1,68 +0,0 @@
"""Integration tests for clice configuration (clice.toml + initializationOptions).
Each workspace's main.cpp references a macro that is only defined when the
rule's `-D<macro>=...` is applied. When rules are applied, compilation is
clean; otherwise an undeclared-identifier diagnostic surfaces.
"""
import pytest
from tests.integration.utils.assertions import (
assert_clean_compile,
assert_has_errors,
get_errors,
)
@pytest.mark.workspace("config_rules_no_config")
async def test_baseline_without_rules(client, workspace):
uri, _ = await client.open_and_wait(workspace / "main.cpp")
assert_has_errors(client, uri, "Expected diagnostics without any rules applied")
errors = get_errors(client.diagnostics[uri])
assert any("FROM_INIT" in (d.message or "") for d in errors), (
f"Expected a diagnostic referencing FROM_INIT, got: {errors}"
)
@pytest.mark.workspace("config_rules_toml")
async def test_rules_from_toml(client, workspace):
uri, _ = await client.open_and_wait(workspace / "main.cpp")
assert_clean_compile(client, uri)
symbols = await client.document_symbols(uri)
assert symbols, "Expected document symbols for value()/main()"
hover = await client.hover_at(uri, line=4, character=4) # on 'main'
assert hover is not None
@pytest.mark.workspace("config_rules_no_config")
@pytest.mark.init_options(
{"rules": [{"patterns": ["**/*.cpp"], "append": ["-DFROM_INIT=1"]}]}
)
async def test_rules_from_init_options(client, workspace):
uri, _ = await client.open_and_wait(workspace / "main.cpp")
assert_clean_compile(client, uri)
@pytest.mark.workspace("config_rules_toml")
@pytest.mark.init_options(
{"rules": [{"patterns": ["**/*.cpp"], "append": ["-DUNRELATED"]}]}
)
async def test_init_options_replaces_toml_rules(client, workspace):
uri, _ = await client.open_and_wait(workspace / "main.cpp")
assert_has_errors(
client, uri, "initializationOptions should have overridden clice.toml rules"
)
errors = get_errors(client.diagnostics[uri])
assert any("FROM_TOML" in (d.message or "") for d in errors), (
f"Expected FROM_TOML diagnostic after override, got: {errors}"
)
@pytest.mark.workspace("config_rules_no_config")
@pytest.mark.init_options(
{"rules": [{"patterns": ["**/does_not_match.cpp"], "append": ["-DFROM_INIT=1"]}]}
)
async def test_rules_pattern_mismatch(client, workspace):
uri, _ = await client.open_and_wait(workspace / "main.cpp")
assert_has_errors(client, uri, "Rule pattern should not have matched main.cpp")

View File

@@ -13,14 +13,13 @@ from lsprotocol.types import (
)
from tests.integration.utils import doc
from tests.integration.utils.wait import IDLE_TIMEOUT
from tests.integration.utils.workspace import did_change
@pytest.mark.workspace("hello_world")
async def test_did_open(client, workspace):
client.open(workspace / "main.cpp")
await asyncio.sleep(IDLE_TIMEOUT)
await asyncio.sleep(5)
@pytest.mark.workspace("hello_world")
@@ -30,13 +29,13 @@ async def test_did_change(client, workspace):
content += "\n"
await asyncio.sleep(0.2)
did_change(client, uri, i + 1, content)
await asyncio.sleep(IDLE_TIMEOUT)
await asyncio.sleep(5)
@pytest.mark.workspace("clang_tidy")
async def test_clang_tidy(client, workspace):
client.open(workspace / "main.cpp")
await asyncio.sleep(IDLE_TIMEOUT)
await asyncio.sleep(5)
@pytest.mark.workspace("hello_world")
@@ -57,7 +56,7 @@ async def test_hover_save_close(client, workspace):
)
)
client.text_document_did_close(DidCloseTextDocumentParams(text_document=doc(uri)))
with pytest.raises(Exception, match="Document not open"):
await client.text_document_hover_async(
HoverParams(text_document=doc(uri), position=Position(line=0, character=0))
)
closed_hover = await client.text_document_hover_async(
HoverParams(text_document=doc(uri), position=Position(line=0, character=0))
)
assert closed_hover is None

View File

@@ -14,7 +14,6 @@ from lsprotocol.types import (
)
from tests.integration.utils.assertions import assert_clean_compile, assert_has_errors
from tests.integration.utils.wait import IDLE_TIMEOUT
@pytest.mark.workspace("modules/single_module_no_deps")
@@ -268,7 +267,7 @@ async def test_circular_module_dependency(client, workspace):
the server remains responsive by opening a non-cyclic file afterwards.
"""
client.open(workspace / "cycle_a.cppm")
await asyncio.sleep(IDLE_TIMEOUT)
await asyncio.sleep(5.0)
uri_ok, _ = await client.open_and_wait(workspace / "ok.cppm")
diags = client.diagnostics.get(uri_ok, [])

View File

@@ -10,7 +10,6 @@ from lsprotocol.types import (
)
from tests.integration.utils import doc
from tests.integration.utils.wait import SETTLE_TIME
from tests.integration.utils.workspace import did_change
@@ -54,7 +53,7 @@ async def test_rapid_edits_with_hover(client, workspace):
await asyncio.sleep(0.02) # ~20ms between edits
# Wait a moment for in-flight requests to settle.
await asyncio.sleep(SETTLE_TIME * 2)
await asyncio.sleep(1.0)
# Final hover must succeed and return correct result.
final_hover = await asyncio.wait_for(

View File

@@ -1,6 +1,6 @@
"""Diagnostic and log message assertion helpers for integration tests."""
"""Diagnostic assertion helpers for integration tests."""
from lsprotocol.types import Diagnostic, DiagnosticSeverity, MessageType
from lsprotocol.types import Diagnostic, DiagnosticSeverity
def get_errors(diagnostics: list[Diagnostic]) -> list[Diagnostic]:
@@ -48,23 +48,3 @@ def assert_clean_compile(client, uri: str) -> None:
"""Assert the file compiled without any diagnostics at all."""
diags = client.diagnostics.get(uri, [])
assert len(diags) == 0, f"Expected clean compile, got: {diags}"
def has_log_message(
client, substring: str, *, severity: MessageType | None = None
) -> bool:
"""Check if any log message contains the given substring."""
for msg in client.log_messages:
if severity is not None and msg.type != severity:
continue
if substring in msg.message:
return True
return False
def assert_no_log_errors(client) -> None:
"""Assert that no error-level log messages were received."""
errors = [m for m in client.log_messages if m.type == MessageType.Error]
assert len(errors) == 0, (
f"Expected no log errors, got: {[e.message for e in errors]}"
)

View File

@@ -7,7 +7,6 @@ from urllib.parse import unquote
from lsprotocol.types import (
PROGRESS,
TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS,
WINDOW_LOG_MESSAGE,
WINDOW_WORK_DONE_PROGRESS_CREATE,
ClientCapabilities,
CodeActionContext,
@@ -25,7 +24,6 @@ from lsprotocol.types import (
InitializeParams,
InitializeResult,
InitializedParams,
LogMessageParams,
Position,
ProgressParams,
PublishDiagnosticsParams,
@@ -50,7 +48,6 @@ class CliceClient(BaseLanguageClient):
super().__init__("clice-test-client", "0.1.0")
self.diagnostics: dict[str, list[Diagnostic]] = {}
self.diagnostics_events: dict[str, asyncio.Event] = {}
self.log_messages: list[LogMessageParams] = []
self.progress_tokens: list[str] = []
self.progress_events: list[dict] = []
self.init_result: InitializeResult | None = None
@@ -67,10 +64,6 @@ class CliceClient(BaseLanguageClient):
if key in self.diagnostics_events:
self.diagnostics_events[key].set()
@self.feature(WINDOW_LOG_MESSAGE)
def on_log_message(params: LogMessageParams) -> None:
self.log_messages.append(params)
@self.feature(WINDOW_WORK_DONE_PROGRESS_CREATE)
def on_create_progress(params: WorkDoneProgressCreateParams) -> None:
token = str(params.token) if isinstance(params.token, int) else params.token
@@ -93,20 +86,16 @@ class CliceClient(BaseLanguageClient):
# ── Lifecycle ────────────────────────────────────────────────────
async def initialize(
self,
workspace: Path,
*,
initialization_options: dict | None = None,
) -> InitializeResult:
params = InitializeParams(
capabilities=ClientCapabilities(),
root_uri=workspace.as_uri(),
workspace_folders=[WorkspaceFolder(uri=workspace.as_uri(), name="test")],
async def initialize(self, workspace: Path) -> InitializeResult:
result = await self.initialize_async(
InitializeParams(
capabilities=ClientCapabilities(),
root_uri=workspace.as_uri(),
workspace_folders=[
WorkspaceFolder(uri=workspace.as_uri(), name="test")
],
)
)
if initialization_options is not None:
params.initialization_options = initialization_options
result = await self.initialize_async(params)
self.initialized(InitializedParams())
self.init_result = result
return result

View File

@@ -9,11 +9,6 @@ from lsprotocol.types import (
WorkspaceSymbolParams,
)
# Standard timing constants — use these instead of hardcoded sleep values.
MTIME_GRANULARITY = 1.1 # Filesystem mtime precision (1s on many FSes, +0.1 margin)
SETTLE_TIME = 0.5 # Time for server to stabilize after an operation
IDLE_TIMEOUT = 5.0 # Time to wait for server idle in lifecycle tests
async def wait_for_recompile(client, uri: str, *, timeout: float = 60.0) -> None:
"""Trigger recompilation via hover and wait for fresh diagnostics.

View File

@@ -1,5 +1,3 @@
[pytest]
asyncio_mode = auto
markers =
workspace
init_options
markers = workspace

View File

@@ -13,9 +13,6 @@ import re
import signal
import sys
import time
# Force line-buffered stdout so CI sees output immediately.
sys.stdout.reconfigure(line_buffering=True)
from pathlib import Path
from urllib.parse import quote, unquote
@@ -112,9 +109,7 @@ async def write_lsp_message(writer: asyncio.StreamWriter, payload: str):
await writer.drain()
async def replay_one(
trace_path: Path, clice_bin: Path, timeout: int, wall_timeout: int = 300
) -> bool | None:
async def replay_one(trace_path: Path, clice_bin: Path, timeout: int) -> bool | None:
"""Replay a single trace. Returns True=PASS, False=FAIL, None=SKIP."""
records = load_trace(trace_path)
if not records:
@@ -184,21 +179,8 @@ async def replay_one(
last_method = None
sent_count = 0
wall_deadline = wall_start + wall_timeout
def remaining_wall():
return max(0, wall_deadline - time.monotonic())
try:
for i, rec in enumerate(records):
if remaining_wall() <= 0:
elapsed = time.monotonic() - wall_start
print(
f" result: TIMEOUT (wall-clock {wall_timeout}s exceeded, {elapsed:.1f}s)"
)
success = False
break
if i > 0:
delay = rec["ts"] - records[i - 1]["ts"]
if delay > 0:
@@ -214,7 +196,7 @@ async def replay_one(
try:
await asyncio.wait_for(
asyncio.gather(*pending.values(), return_exceptions=True),
timeout=min(timeout, remaining_wall()),
timeout=timeout,
)
except asyncio.TimeoutError:
elapsed = time.monotonic() - wall_start
@@ -228,19 +210,7 @@ async def replay_one(
if msg_id is not None and method is not None:
pending[msg_id] = asyncio.get_event_loop().create_future()
try:
await asyncio.wait_for(
write_lsp_message(proc.stdin, rec["msg"]),
timeout=min(30, remaining_wall()),
)
except asyncio.TimeoutError:
elapsed = time.monotonic() - wall_start
print(
f" result: HANG (write blocked at {last_method},"
f" sent={sent_count}/{len(records)}, {elapsed:.1f}s)"
)
success = False
break
await write_lsp_message(proc.stdin, rec["msg"])
sent_count = i + 1
except (ConnectionError, BrokenPipeError):
@@ -261,7 +231,7 @@ async def replay_one(
try:
await asyncio.wait_for(
asyncio.gather(*pending.values(), return_exceptions=True),
timeout=min(timeout, remaining_wall()),
timeout=timeout,
)
except asyncio.TimeoutError:
elapsed = time.monotonic() - wall_start
@@ -324,7 +294,7 @@ async def async_main(args):
print(f"SKIP: {trace} (not found)")
skipped += 1
continue
result = await replay_one(trace, args.clice, args.timeout, args.wall_timeout)
result = await replay_one(trace, args.clice, args.timeout)
if result is None:
skipped += 1
elif result:
@@ -347,16 +317,7 @@ def main():
p.add_argument("traces", nargs="+", type=Path, help="JSONL trace files")
p.add_argument("--clice", required=True, type=Path, help="Path to clice binary")
p.add_argument(
"--timeout",
type=int,
default=120,
help="Per-request timeout in seconds (default: 120)",
)
p.add_argument(
"--wall-timeout",
type=int,
default=300,
help="Max wall-clock time per trace in seconds (default: 300)",
"--timeout", type=int, default=120, help="Timeout in seconds (default: 120)"
)
args = p.parse_args()
sys.exit(asyncio.run(async_main(args)))

View File

@@ -9,7 +9,7 @@ namespace clice::testing {
namespace {
namespace protocol = kota::ipc::protocol;
namespace protocol = eventide::ipc::protocol;
TEST_SUITE(CodeCompletion) {
@@ -233,33 +233,6 @@ void bar() {
}
}
TEST_CASE(DeprecatedTag) {
code_complete(R"cpp(
[[deprecated]] int foooo(int x);
int z = fo$(pos)
)cpp");
auto it = find_item("foooo");
ASSERT_TRUE(it != items.end());
ASSERT_TRUE(it->tags.has_value());
auto& tags = *it->tags;
ASSERT_TRUE(std::ranges::find(tags, protocol::CompletionItemTag::Deprecated) != tags.end());
}
TEST_CASE(NotDeprecated) {
code_complete(R"cpp(
int foooo(int x);
int z = fo$(pos)
)cpp");
auto it = find_item("foooo");
ASSERT_TRUE(it != items.end());
// Non-deprecated should have no Deprecated tag.
ASSERT_TRUE(!it->tags.has_value() ||
std::ranges::find(*it->tags, protocol::CompletionItemTag::Deprecated) ==
it->tags->end());
}
TEST_CASE(NoBundleOverloads) {
feature::CodeCompletionOptions opts;
opts.bundle_overloads = false;

View File

@@ -9,7 +9,7 @@ namespace clice::testing {
namespace {
namespace protocol = kota::ipc::protocol;
namespace protocol = eventide::ipc::protocol;
TEST_SUITE(DocumentLink, Tester) {
@@ -21,6 +21,11 @@ void run(llvm::StringRef source, llvm::StringRef standard = "-std=c++17") {
links = feature::document_links(*unit, feature::PositionEncoding::UTF8);
}
auto to_local_range(const protocol::Range& range) -> LocalSourceRange {
feature::PositionMapper converter(unit->interested_content(), feature::PositionEncoding::UTF8);
return LocalSourceRange(*converter.to_offset(range.start), *converter.to_offset(range.end));
}
void EXPECT_LINK(std::size_t index, llvm::StringRef name, llvm::StringRef path) {
auto& link = links[index];
auto expected = range(name, "main.cpp");

View File

@@ -11,7 +11,7 @@ namespace clice::testing {
namespace {
namespace protocol = kota::ipc::protocol;
namespace protocol = eventide::ipc::protocol;
TEST_SUITE(DocumentSymbol, Tester) {

View File

@@ -9,7 +9,7 @@ namespace clice::testing {
namespace {
namespace protocol = kota::ipc::protocol;
namespace protocol = eventide::ipc::protocol;
TEST_SUITE(FoldingRange, Tester) {
@@ -37,10 +37,19 @@ void run(llvm::StringRef code) {
}
auto to_local_range(const protocol::FoldingRange& range) -> LocalSourceRange {
return Tester::to_local_range(protocol::Range{
.start = {.line = range.start_line, .character = range.start_character.value_or(0)},
.end = {.line = range.end_line, .character = range.end_character.value_or(0) },
});
feature::PositionMapper converter(unit->interested_content(), feature::PositionEncoding::UTF8);
auto start = protocol::Position{
.line = range.start_line,
.character = range.start_character.value_or(0),
};
auto end = protocol::Position{
.line = range.end_line,
.character = range.end_character.value_or(0),
};
return LocalSourceRange(*converter.to_offset(start), *converter.to_offset(end));
}
void EXPECT_FOLDING(std::uint32_t index,

View File

@@ -8,7 +8,7 @@ namespace clice::testing {
namespace {
namespace protocol = kota::ipc::protocol;
namespace protocol = eventide::ipc::protocol;
TEST_SUITE(Hover, Tester) {

View File

@@ -8,7 +8,7 @@ namespace clice::testing {
namespace {
namespace protocol = kota::ipc::protocol;
namespace protocol = eventide::ipc::protocol;
TEST_SUITE(InlayHint, Tester) {

View File

@@ -13,7 +13,7 @@ namespace clice::testing {
namespace {
namespace protocol = kota::ipc::protocol;
namespace protocol = eventide::ipc::protocol;
struct DecodedToken {
LocalSourceRange range;
@@ -423,122 +423,6 @@ cd*/
ASSERT_EQ(comments[1].length, 4);
}
TEST_CASE(ModuleDeclaration) {
add_main("main.cpp", R"cpp(
export @kw[module] @mod[foo];
)cpp");
ASSERT_TRUE(compile("-std=c++20"));
tokens = feature::semantic_tokens(*unit, feature::PositionEncoding::UTF8);
decoded = decode_utf8_tokens(unit->interested_content(), tokens);
EXPECT_TOKEN("kw", SymbolKind::Keyword);
EXPECT_TOKEN("mod", SymbolKind::Module);
}
TEST_CASE(ModuleDeclarationDotted) {
add_main("main.cpp", R"cpp(
export @kw[module] @m0[foo].@m1[bar];
)cpp");
ASSERT_TRUE(compile("-std=c++20"));
tokens = feature::semantic_tokens(*unit, feature::PositionEncoding::UTF8);
decoded = decode_utf8_tokens(unit->interested_content(), tokens);
EXPECT_TOKEN("kw", SymbolKind::Keyword);
EXPECT_TOKEN("m0", SymbolKind::Module);
EXPECT_TOKEN("m1", SymbolKind::Module);
}
TEST_CASE(ModuleImport) {
add_files("main.cpp", R"(
#[mod.cppm]
export module foo;
export int x = 42;
#[main.cpp]
@kw[import] @mod[foo];
int y = x;
)");
ASSERT_TRUE(compile_with_modules());
tokens = feature::semantic_tokens(*unit, feature::PositionEncoding::UTF8);
decoded = decode_utf8_tokens(unit->interested_content(), tokens);
EXPECT_TOKEN("kw", SymbolKind::Keyword);
EXPECT_TOKEN("mod", SymbolKind::Module);
}
TEST_CASE(ModulePartition) {
add_main("main.cpp", R"cpp(
export module @m0[foo]:@m1[bar];
)cpp");
ASSERT_TRUE(compile("-std=c++20"));
tokens = feature::semantic_tokens(*unit, feature::PositionEncoding::UTF8);
decoded = decode_utf8_tokens(unit->interested_content(), tokens);
EXPECT_TOKEN("m0", SymbolKind::Module);
EXPECT_TOKEN("m1", SymbolKind::Module);
}
TEST_CASE(ModuleReexport) {
add_files("main.cppm", R"(
#[mod.cppm]
export module foo;
export int x = 42;
#[main.cppm]
export module bar;
export @kw[import] @mod[foo];
)");
ASSERT_TRUE(compile_with_modules());
tokens = feature::semantic_tokens(*unit, feature::PositionEncoding::UTF8);
decoded = decode_utf8_tokens(unit->interested_content(), tokens);
EXPECT_TOKEN("kw", SymbolKind::Keyword);
EXPECT_TOKEN("mod", SymbolKind::Module);
}
TEST_CASE(GlobalModuleFragment) {
add_main("main.cpp", R"cpp(
module;
export module @mod[foo];
)cpp");
ASSERT_TRUE(compile("-std=c++20"));
tokens = feature::semantic_tokens(*unit, feature::PositionEncoding::UTF8);
decoded = decode_utf8_tokens(unit->interested_content(), tokens);
EXPECT_TOKEN("mod", SymbolKind::Module);
}
TEST_CASE(PrivateModuleFragment) {
add_main("main.cpp", R"cpp(
export module @mod[foo];
module :private;
int x = 1;
)cpp");
ASSERT_TRUE(compile("-std=c++20"));
tokens = feature::semantic_tokens(*unit, feature::PositionEncoding::UTF8);
decoded = decode_utf8_tokens(unit->interested_content(), tokens);
EXPECT_TOKEN("mod", SymbolKind::Module);
}
TEST_CASE(ModuleKeywordAsIdentifier) {
run_utf8(R"cpp(
void f() {
struct @s0[module] {};
@s1[module] @v0[m];
int @v1[import] = 1;
int @v2[module] = 2;
}
)cpp");
auto definition = modifier_mask({SymbolModifiers::Definition});
EXPECT_TOKEN("s0", SymbolKind::Struct, definition);
EXPECT_TOKEN("s1", SymbolKind::Struct);
EXPECT_TOKEN("v0", SymbolKind::Variable, definition);
EXPECT_TOKEN("v1", SymbolKind::Variable, definition);
EXPECT_TOKEN("v2", SymbolKind::Variable, definition);
}
}; // TEST_SUITE(SemanticTokens)
} // namespace

View File

@@ -6,7 +6,7 @@ namespace clice::testing {
namespace {
namespace protocol = kota::ipc::protocol;
namespace protocol = eventide::ipc::protocol;
TEST_SUITE(SignatureHelp, Tester) {

View File

@@ -11,6 +11,8 @@
namespace clice::testing {
namespace {
namespace et = eventide;
/// Build a dispatch_fn that compiles PCMs in-process (no workers).
/// Clang requires ALL transitive PCM deps (not just direct imports)
/// in PrebuiltModuleFiles, so we pass every available PCM.
@@ -18,7 +20,7 @@ CompileGraph::dispatch_fn make_dispatch(CompilationDatabase& cdb,
PathPool& pool,
DependencyGraph& graph,
llvm::DenseMap<std::uint32_t, std::string>& pcm_paths) {
return [&](std::uint32_t path_id) -> kota::task<bool> {
return [&](std::uint32_t path_id) -> et::task<bool> {
auto file_path = pool.resolve(path_id);
auto results = cdb.lookup(file_path, {.query_toolchain = true, .suppress_logging = true});
if(results.empty()) {
@@ -121,8 +123,8 @@ TEST_CASE(SingleModuleNoDeps) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
kota::event_loop loop;
auto test = [this, &cg, &env, pid_a]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, pid_a]() -> et::task<> {
auto result = co_await cg.compile(pid_a).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -155,8 +157,8 @@ TEST_CASE(ChainedModules) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
kota::event_loop loop;
auto test = [this, &cg, &env, pid_a, pid_b]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, pid_a, pid_b]() -> et::task<> {
auto result = co_await cg.compile(pid_b).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -200,8 +202,8 @@ TEST_CASE(DiamondModules) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
kota::event_loop loop;
auto test = [this, &cg, &env, pid_top]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, pid_top]() -> et::task<> {
auto result = co_await cg.compile(pid_top).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -236,8 +238,8 @@ TEST_CASE(DottedModuleName) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
kota::event_loop loop;
auto test = [this, &cg, &env, pid_app]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, pid_app]() -> et::task<> {
auto result = co_await cg.compile(pid_app).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -278,8 +280,8 @@ TEST_CASE(ReExport) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
kota::event_loop loop;
auto test = [this, &cg, &env, pid_user]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, pid_user]() -> et::task<> {
auto result = co_await cg.compile(pid_user).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -322,8 +324,8 @@ TEST_CASE(ExportBlock) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
kota::event_loop loop;
auto test = [this, &cg, &env, pid]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, pid]() -> et::task<> {
auto result = co_await cg.compile(pid).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -358,8 +360,8 @@ TEST_CASE(GlobalModuleFragment) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
kota::event_loop loop;
auto test = [this, &cg, &env, pid]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, pid]() -> et::task<> {
auto result = co_await cg.compile(pid).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -394,8 +396,8 @@ TEST_CASE(PrivateModuleFragment) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
kota::event_loop loop;
auto test = [this, &cg, &env, pid]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, pid]() -> et::task<> {
auto result = co_await cg.compile(pid).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -434,8 +436,8 @@ TEST_CASE(PartitionInterface) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
kota::event_loop loop;
auto test = [this, &cg, &env, pid_m]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, pid_m]() -> et::task<> {
auto result = co_await cg.compile(pid_m).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -474,8 +476,8 @@ TEST_CASE(MultiplePartitions) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
kota::event_loop loop;
auto test = [this, &cg, &env, pid_lib]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, pid_lib]() -> et::task<> {
auto result = co_await cg.compile(pid_lib).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -517,8 +519,8 @@ TEST_CASE(PartitionChain) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
kota::event_loop loop;
auto test = [this, &cg, &env, pid_sys]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, pid_sys]() -> et::task<> {
auto result = co_await cg.compile(pid_sys).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -559,8 +561,8 @@ TEST_CASE(ExportNamespace) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
kota::event_loop loop;
auto test = [this, &cg, &env, pid]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, pid]() -> et::task<> {
auto result = co_await cg.compile(pid).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -598,8 +600,8 @@ TEST_CASE(GMFWithImport) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
kota::event_loop loop;
auto test = [this, &cg, &env, pid]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, pid]() -> et::task<> {
auto result = co_await cg.compile(pid).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -649,8 +651,8 @@ TEST_CASE(DeepChain) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
kota::event_loop loop;
auto test = [this, &cg, &env, pid]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, pid]() -> et::task<> {
auto result = co_await cg.compile(pid).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -684,8 +686,8 @@ TEST_CASE(IndependentModules) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
kota::event_loop loop;
auto test = [this, &cg, &env, pid_x, pid_y]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, pid_x, pid_y]() -> et::task<> {
auto r1 = co_await cg.compile(pid_x).catch_cancel();
EXPECT_TRUE(r1.has_value() && *r1);
auto r2 = co_await cg.compile(pid_y).catch_cancel();
@@ -726,8 +728,8 @@ TEST_CASE(TemplateExport) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
kota::event_loop loop;
auto test = [this, &cg, &env, pid]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, pid]() -> et::task<> {
auto result = co_await cg.compile(pid).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -773,8 +775,8 @@ TEST_CASE(ClassExportAndInheritance) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
kota::event_loop loop;
auto test = [this, &cg, &env, pid]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, pid]() -> et::task<> {
auto result = co_await cg.compile(pid).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -811,8 +813,8 @@ TEST_CASE(RecompileAfterUpdate) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
kota::event_loop loop;
auto test = [this, &cg, &env, pid_leaf, pid_mid]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, pid_leaf, pid_mid]() -> et::task<> {
// First compile.
auto r1 = co_await cg.compile(pid_mid).catch_cancel();
EXPECT_TRUE(r1.has_value() && *r1);
@@ -862,8 +864,8 @@ TEST_CASE(PartitionWithGMF) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
kota::event_loop loop;
auto test = [this, &cg, &env, pid]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, pid]() -> et::task<> {
auto result = co_await cg.compile(pid).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -903,8 +905,8 @@ TEST_CASE(PartitionWithExternalImport) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
kota::event_loop loop;
auto test = [this, &cg, &env, pid_app]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, pid_app]() -> et::task<> {
auto result = co_await cg.compile(pid_app).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -956,8 +958,8 @@ TEST_CASE(DiamondUpdateCascade) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
kota::event_loop loop;
auto test = [this, &cg, &env, pid_base, pid_left, pid_right, pid_top]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, pid_base, pid_left, pid_right, pid_top]() -> et::task<> {
// Initial compile.
auto r1 = co_await cg.compile(pid_top).catch_cancel();
EXPECT_TRUE(r1.has_value() && *r1);
@@ -1044,8 +1046,8 @@ TEST_CASE(ReResolveAfterUpdate) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
std::move(counting_resolver));
kota::event_loop loop;
auto test = [this, &cg, &env, &resolve_count, pid_mid]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, &resolve_count, pid_mid]() -> et::task<> {
// First compile: resolve_fn called once for Mid.
auto r1 = co_await cg.compile(pid_mid).catch_cancel();
EXPECT_TRUE(r1.has_value() && *r1);
@@ -1090,8 +1092,8 @@ TEST_CASE(CompileFailurePropagation) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
kota::event_loop loop;
auto test = [this, &cg, &env, pid_bad]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, pid_bad]() -> et::task<> {
auto result = co_await cg.compile(pid_bad).catch_cancel();
EXPECT_TRUE(result.has_value());
// Compilation should fail due to undefined symbol.
@@ -1131,8 +1133,8 @@ TEST_CASE(ModuleImplementationUnit) {
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
make_resolver(env.cdb, env.pool, env.graph));
kota::event_loop loop;
auto test = [this, &cg, &env, pid_iface]() -> kota::task<> {
et::event_loop loop;
auto test = [this, &cg, &env, pid_iface]() -> et::task<> {
// Build the interface PCM via CompileGraph.
auto r1 = co_await cg.compile(pid_iface).catch_cancel();
EXPECT_TRUE(r1.has_value() && *r1);

View File

@@ -6,6 +6,7 @@
namespace clice::testing {
namespace {
namespace et = eventide;
namespace ranges = std::ranges;
/// A resolve_fn that always returns no dependencies.
@@ -28,27 +29,27 @@ CompileGraph::resolve_fn
}
CompileGraph::dispatch_fn instant_dispatch() {
return [](std::uint32_t) -> kota::task<bool> {
return [](std::uint32_t) -> et::task<bool> {
co_return true;
};
}
CompileGraph::dispatch_fn tracking_dispatch(std::vector<std::uint32_t>& compiled) {
return [&compiled](std::uint32_t path_id) -> kota::task<bool> {
return [&compiled](std::uint32_t path_id) -> et::task<bool> {
compiled.push_back(path_id);
co_return true;
};
}
CompileGraph::dispatch_fn failing_dispatch() {
return [](std::uint32_t) -> kota::task<bool> {
return [](std::uint32_t) -> et::task<bool> {
co_return false;
};
}
/// Dispatch that fails only for specific path_ids.
CompileGraph::dispatch_fn selective_dispatch(llvm::DenseSet<std::uint32_t> fail_ids) {
return [fail_ids = std::move(fail_ids)](std::uint32_t path_id) -> kota::task<bool> {
return [fail_ids = std::move(fail_ids)](std::uint32_t path_id) -> et::task<bool> {
co_return !fail_ids.contains(path_id);
};
}
@@ -60,7 +61,7 @@ std::optional<CompileGraph> graph;
template <typename F>
void execute(F&& fn) {
kota::event_loop loop;
et::event_loop loop;
auto t = fn();
loop.schedule(t);
loop.run();
@@ -69,7 +70,7 @@ void execute(F&& fn) {
TEST_CASE(CompileNoDeps) {
graph.emplace(tracking_dispatch(compiled), no_deps());
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
auto result = co_await graph->compile(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -86,7 +87,7 @@ TEST_CASE(CompileWithDependency) {
{1, {2}}
}));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
auto result = co_await graph->compile(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -108,7 +109,7 @@ TEST_CASE(CompileChain) {
{2, {3}}
}));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
auto result = co_await graph->compile(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -131,7 +132,7 @@ TEST_CASE(DiamondDependency) {
{3, {4} }
}));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
auto result = co_await graph->compile(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -151,7 +152,7 @@ TEST_CASE(UpdateInvalidates) {
{1, {2}}
}));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
co_await graph->compile(1).catch_cancel();
EXPECT_FALSE(graph->is_dirty(2));
EXPECT_FALSE(graph->is_dirty(1));
@@ -171,7 +172,7 @@ TEST_CASE(UpdateCascade) {
{2, {3}}
}));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
co_await graph->compile(1).catch_cancel();
EXPECT_FALSE(graph->is_dirty(2));
EXPECT_FALSE(graph->is_dirty(3));
@@ -191,7 +192,7 @@ TEST_CASE(CompileAfterUpdate) {
{1, {2}}
}));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
co_await graph->compile(1).catch_cancel();
EXPECT_EQ(compiled.size(), 2u);
@@ -209,7 +210,7 @@ TEST_CASE(DispatchFailure) {
{1, {2}}
}));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
auto result = co_await graph->compile(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_FALSE(*result);
@@ -227,7 +228,7 @@ TEST_CASE(CancelAll) {
TEST_CASE(SecondCompileSkips) {
graph.emplace(tracking_dispatch(compiled), no_deps());
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
co_await graph->compile(1).catch_cancel();
EXPECT_EQ(compiled.size(), 1u);
// Second compile should skip (already clean).
@@ -244,7 +245,7 @@ TEST_CASE(CascadeThroughAlreadyDirty) {
{2, {3}}
}));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
co_await graph->compile(1).catch_cancel();
// Update node 2: marks 2 and 1 dirty.
@@ -269,7 +270,7 @@ TEST_CASE(CircularDependencyDetection) {
{2, {1}}
}));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
auto result = co_await graph->compile(1).catch_cancel();
// Should return false (cycle detected), not deadlock.
EXPECT_TRUE(result.has_value());
@@ -288,7 +289,7 @@ TEST_CASE(CrossBranchCycleDetection) {
{3, {2} }
}));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
auto result = co_await graph->compile(1).catch_cancel();
// Should return false (cycle detected), not deadlock.
EXPECT_TRUE(result.has_value());
@@ -311,7 +312,7 @@ TEST_CASE(UpdateResetsResolved) {
graph.emplace(tracking_dispatch(compiled), std::move(resolver));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
// First compile: resolves 1 -> {2}.
co_await graph->compile(1).catch_cancel();
EXPECT_EQ(resolve_count, 1);
@@ -343,7 +344,7 @@ TEST_CASE(UpdateCleansBackEdges) {
graph.emplace(tracking_dispatch(compiled), std::move(resolver));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
// First compile: 1 -> {2}.
co_await graph->compile(1).catch_cancel();
EXPECT_FALSE(graph->is_dirty(1));
@@ -372,7 +373,7 @@ TEST_CASE(DiamondUpdateCascade) {
{3, {4} }
}));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
co_await graph->compile(1).catch_cancel();
EXPECT_FALSE(graph->is_dirty(1));
EXPECT_FALSE(graph->is_dirty(4));
@@ -401,7 +402,7 @@ TEST_CASE(UpdateReturnsAllDirtied) {
{2, {3}}
}));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
co_await graph->compile(1).catch_cancel();
auto dirtied = graph->update(3);
@@ -416,7 +417,7 @@ TEST_CASE(UpdateReturnsAllDirtied) {
TEST_CASE(HasUnitAndIsCompiling) {
graph.emplace(instant_dispatch(), no_deps());
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
EXPECT_FALSE(graph->has_unit(1));
EXPECT_FALSE(graph->is_compiling(1));
@@ -433,7 +434,7 @@ TEST_CASE(FailureLeavesDepsDirty) {
{1, {2}}
}));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
auto result = co_await graph->compile(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_FALSE(*result);
@@ -450,7 +451,7 @@ TEST_CASE(SelfLoop) {
{1, {1}}
}));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
auto result = co_await graph->compile(1).catch_cancel();
// Should detect cycle and return false, not deadlock.
EXPECT_TRUE(result.has_value());
@@ -464,7 +465,7 @@ TEST_CASE(CancelAllAndRecompile) {
{1, {2}}
}));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
co_await graph->compile(1).catch_cancel();
EXPECT_EQ(compiled.size(), 2u);
EXPECT_FALSE(graph->is_dirty(1));
@@ -487,10 +488,10 @@ TEST_CASE(CancelAllAndRecompile) {
}
TEST_CASE(UpdateDuringCompile) {
kota::event_loop loop;
kota::event gate;
et::event_loop loop;
et::event gate;
auto gated_dispatch = [&gate](std::uint32_t) -> kota::task<bool> {
auto gated_dispatch = [&gate](std::uint32_t) -> et::task<bool> {
co_await gate.wait();
co_return true;
};
@@ -501,14 +502,14 @@ TEST_CASE(UpdateDuringCompile) {
bool was_cancelled = false;
// Coroutine 1: compile(1), will suspend inside dispatch waiting on gate.
auto compiler = [&]() -> kota::task<> {
auto compiler = [&]() -> et::task<> {
auto result = co_await graph->compile(1).catch_cancel();
compile_done = true;
was_cancelled = !result.has_value();
};
// Coroutine 2: update(1) while dispatch is in flight, then unblock gate.
auto updater = [&]() -> kota::task<> {
auto updater = [&]() -> et::task<> {
graph->update(1);
gate.set();
co_return;
@@ -533,7 +534,7 @@ TEST_CASE(WhenAllPartialFailure) {
}),
static_resolver({{1, {2, 3}}}));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
auto result = co_await graph->compile(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_FALSE(*result);
@@ -565,7 +566,7 @@ TEST_CASE(EmptyGraphNoCompile) {
TEST_CASE(CompileDepsNoDeps) {
graph.emplace(tracking_dispatch(compiled), no_deps());
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
auto result = co_await graph->compile_deps(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -581,7 +582,7 @@ TEST_CASE(CompileDepsWithDependency) {
{1, {2}}
}));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
auto result = co_await graph->compile_deps(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -601,7 +602,7 @@ TEST_CASE(CompileDepsChain) {
{2, {3}}
}));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
auto result = co_await graph->compile_deps(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -622,7 +623,7 @@ TEST_CASE(CompileDepsDiamond) {
{3, {4} }
}));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
auto result = co_await graph->compile_deps(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -639,7 +640,7 @@ TEST_CASE(CompileDepsDiamond) {
TEST_CASE(CompileDepsFailure) {
// 1 -> 2. Dispatch fails for unit 2.
auto fail_and_track = [&](std::uint32_t path_id) -> kota::task<bool> {
auto fail_and_track = [&](std::uint32_t path_id) -> et::task<bool> {
compiled.push_back(path_id);
co_return false;
};
@@ -649,7 +650,7 @@ TEST_CASE(CompileDepsFailure) {
{1, {2}}
}));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
auto result = co_await graph->compile_deps(1).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_FALSE(*result);
@@ -665,7 +666,7 @@ TEST_CASE(CompileDepsPlainCpp) {
{10, {20}}
}));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
auto result = co_await graph->compile_deps(10).catch_cancel();
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(*result);
@@ -687,11 +688,11 @@ TEST_CASE(CompileDepsConcurrentDedup) {
{2, {3, 5}},
}));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
// Launch both compile_deps concurrently.
auto t1 = graph->compile_deps(1);
auto t2 = graph->compile_deps(2);
auto results = co_await kota::when_all(std::move(t1), std::move(t2));
auto results = co_await et::when_all(std::move(t1), std::move(t2));
auto [r1, r2] = results;
EXPECT_TRUE(r1);
@@ -721,10 +722,10 @@ TEST_CASE(CompileDepsResolveOnce) {
graph.emplace(tracking_dispatch(compiled), std::move(resolve));
execute([&]() -> kota::task<> {
execute([&]() -> et::task<> {
auto t1 = graph->compile_deps(1);
auto t2 = graph->compile_deps(2);
auto results = co_await kota::when_all(std::move(t1), std::move(t2));
auto results = co_await et::when_all(std::move(t1), std::move(t2));
auto [r1, r2] = results;
EXPECT_TRUE(r1);

View File

@@ -1,501 +0,0 @@
#include <cstdlib>
#include "test/temp_dir.h"
#include "test/test.h"
#include "server/config.h"
#include "support/filesystem.h"
#include "kota/codec/json/json.h"
#include "kota/codec/toml/toml.h"
namespace clice::testing {
// POSIX setenv/unsetenv don't exist on Windows; map to _putenv_s
// (passing an empty value to _putenv_s removes the variable).
static void set_env(const char* name, const char* value) {
#ifdef _WIN32
::_putenv_s(name, value);
#else
::setenv(name, value, 1);
#endif
}
static void unset_env(const char* name) {
#ifdef _WIN32
::_putenv_s(name, "");
#else
::unsetenv(name);
#endif
}
TEST_SUITE(Config) {
TEST_CASE(ParsePartialProject) {
auto result = kota::codec::toml::parse<ProjectConfig>(R"(cache_dir = "/tmp/test")");
EXPECT_TRUE(result.has_value());
EXPECT_EQ(std::string_view(result->cache_dir), "/tmp/test");
EXPECT_EQ(result->clang_tidy.value, false);
EXPECT_EQ(result->max_active_file.value, 0);
EXPECT_FALSE(result->enable_indexing.has_value());
EXPECT_FALSE(result->idle_timeout_ms.has_value());
}
TEST_CASE(ParseConfigRule) {
auto result = kota::codec::toml::parse<ConfigRule>(R"(
patterns = ["**/*.cpp"]
append = ["-std=c++20"]
)");
EXPECT_TRUE(result.has_value());
EXPECT_EQ(result->patterns.size(), 1u);
EXPECT_EQ(result->patterns[0], "**/*.cpp");
EXPECT_EQ(result->append[0], "-std=c++20");
EXPECT_TRUE(result->remove.empty());
}
TEST_CASE(ParseFullConfig) {
auto result = kota::codec::toml::parse<Config>(R"(
[project]
cache_dir = "/tmp/test"
clang_tidy = true
enable_indexing = false
[[rules]]
patterns = ["**/*.cpp"]
append = ["-std=c++20"]
)");
EXPECT_TRUE(result.has_value());
EXPECT_EQ(std::string_view(result->project.cache_dir), "/tmp/test");
EXPECT_EQ(result->project.clang_tidy.value, true);
EXPECT_EQ(*result->project.enable_indexing, false);
EXPECT_EQ(result->rules.size(), 1u);
EXPECT_EQ(result->rules[0].patterns[0], "**/*.cpp");
}
TEST_CASE(ParseEmptyConfig) {
auto result = kota::codec::toml::parse<Config>("");
EXPECT_TRUE(result.has_value());
EXPECT_TRUE(result->rules.empty());
EXPECT_TRUE(std::string_view(result->project.cache_dir).empty());
}
TEST_CASE(ParseOnlyRules) {
auto result = kota::codec::toml::parse<Config>(R"(
[[rules]]
patterns = ["*.h"]
remove = ["-Werror"]
)");
EXPECT_TRUE(result.has_value());
EXPECT_EQ(result->rules.size(), 1u);
EXPECT_EQ(result->rules[0].patterns[0], "*.h");
EXPECT_EQ(result->rules[0].remove[0], "-Werror");
EXPECT_TRUE(std::string_view(result->project.cache_dir).empty());
}
TEST_CASE(MatchRulesBasic) {
Config config;
config.rules.push_back(ConfigRule{
.patterns = {"**/*.cpp"},
.append = {"-std=c++20"},
.remove = {"-std=c++17"},
});
config.apply_defaults("");
std::vector<std::string> append, remove;
config.match_rules("/src/foo.cpp", append, remove);
EXPECT_EQ(append.size(), 1u);
EXPECT_EQ(append[0], "-std=c++20");
EXPECT_EQ(remove.size(), 1u);
EXPECT_EQ(remove[0], "-std=c++17");
}
TEST_CASE(MatchRulesNoMatch) {
Config config;
config.rules.push_back(ConfigRule{
.patterns = {"**/*.cpp"},
.append = {"-DFOO"},
});
config.apply_defaults("");
std::vector<std::string> append, remove;
config.match_rules("/src/foo.h", append, remove);
EXPECT_TRUE(append.empty());
EXPECT_TRUE(remove.empty());
}
TEST_CASE(MatchRulesMultiple) {
Config config;
config.rules.push_back(ConfigRule{
.patterns = {"**/*.cpp"},
.append = {"-DCPP"},
});
config.rules.push_back(ConfigRule{
.patterns = {"**/test_*.cpp"},
.append = {"-DTEST"},
});
config.apply_defaults("");
std::vector<std::string> append, remove;
config.match_rules("/src/test_foo.cpp", append, remove);
EXPECT_EQ(append.size(), 2u);
EXPECT_EQ(append[0], "-DCPP");
EXPECT_EQ(append[1], "-DTEST");
}
TEST_CASE(ApplyDefaults) {
Config config;
config.apply_defaults("/workspace");
EXPECT_EQ(*config.project.enable_indexing, true);
EXPECT_EQ(*config.project.idle_timeout_ms, 3000);
EXPECT_EQ(config.project.max_active_file.value, 8);
EXPECT_EQ(config.project.stateful_worker_count.value, 2u);
EXPECT_GE(config.project.stateless_worker_count.value, 2u);
EXPECT_FALSE(config.project.cache_dir.empty());
EXPECT_FALSE(config.project.index_dir.empty());
EXPECT_FALSE(config.project.logging_dir.empty());
}
TEST_CASE(ApplyDefaultsEmptyWorkspace) {
Config config;
config.apply_defaults("");
EXPECT_TRUE(config.project.cache_dir.empty());
EXPECT_TRUE(config.project.index_dir.empty());
EXPECT_TRUE(config.project.logging_dir.empty());
}
TEST_CASE(ApplyDefaultsPreserveSet) {
Config config;
config.project.cache_dir = "/custom";
config.project.enable_indexing = false;
config.apply_defaults("/workspace");
EXPECT_EQ(std::string_view(config.project.cache_dir), "/custom");
EXPECT_EQ(*config.project.enable_indexing, false);
}
TEST_CASE(LoadFromJson) {
auto result = Config::load_from_json(R"({
"project": {
"cache_dir": "/opt/cache",
"clang_tidy": true,
"enable_indexing": false
},
"rules": [
{ "patterns": ["**/*.cpp"], "append": ["-DFOO"] }
]
})",
"/workspace");
EXPECT_TRUE(result.has_value());
EXPECT_EQ(std::string_view(result->project.cache_dir), "/opt/cache");
EXPECT_EQ(result->project.clang_tidy.value, true);
EXPECT_EQ(*result->project.enable_indexing, false);
EXPECT_EQ(result->rules.size(), 1u);
EXPECT_EQ(result->compiled_rules.size(), 1u);
}
TEST_CASE(LoadFromJsonInvalid) {
auto result = Config::load_from_json("{not valid json", "/workspace");
EXPECT_FALSE(result.has_value());
}
TEST_CASE(LoadMalformedToml) {
TempDir tmp;
tmp.touch("clice.toml", "[project\nbroken");
auto result = Config::load(tmp.path("clice.toml"), tmp.root.str().str());
EXPECT_FALSE(result.has_value());
}
TEST_CASE(LoadMissingFile) {
auto result = Config::load("/nonexistent/clice.toml", "/workspace");
EXPECT_FALSE(result.has_value());
}
TEST_CASE(WorkspaceVarSubst) {
Config config;
config.project.cache_dir = "${workspace}/cache";
config.project.index_dir = "${workspace}/idx";
config.project.logging_dir = "${workspace}/logs";
config.project.compile_commands_paths = {"${workspace}/build"};
config.apply_defaults("/my/ws");
EXPECT_EQ(std::string_view(config.project.cache_dir), "/my/ws/cache");
EXPECT_EQ(std::string_view(config.project.index_dir), "/my/ws/idx");
EXPECT_EQ(std::string_view(config.project.logging_dir), "/my/ws/logs");
EXPECT_EQ(config.project.compile_commands_paths[0], "/my/ws/build");
}
TEST_CASE(XdgCacheDir) {
TempDir tmp;
auto cache_base = tmp.path("xdg");
set_env("XDG_CACHE_HOME", cache_base.c_str());
Config config;
config.apply_defaults("/some/ws");
unset_env("XDG_CACHE_HOME");
// Normalize separators: on Windows path::join uses '\\' but the test
// expects posix-style comparisons.
std::string cache = path::convert_to_slash(std::string_view(config.project.cache_dir));
std::string base = path::convert_to_slash(cache_base);
EXPECT_TRUE(llvm::StringRef(cache).starts_with(base));
EXPECT_TRUE(cache.find("/clice/") != std::string::npos);
}
TEST_CASE(InvalidGlobPattern) {
Config config;
// All-invalid patterns: rule must be dropped entirely, not appended as empty.
config.rules.push_back(ConfigRule{
.patterns = {"**/****.{c,cc}"},
.append = {"-DSHOULD_NOT_APPEAR"},
});
// Mixed valid/invalid: only the invalid pattern is skipped; rule remains.
config.rules.push_back(ConfigRule{
.patterns = {"**/****.{c,cc}", "**/*.cpp"},
.append = {"-DCPP"},
});
config.apply_defaults("");
EXPECT_EQ(config.compiled_rules.size(), 1u);
std::vector<std::string> append, remove;
config.match_rules("/src/foo.cpp", append, remove);
EXPECT_EQ(append.size(), 1u);
EXPECT_EQ(append[0], "-DCPP");
}
TEST_CASE(ConfigPriorityJson) {
// initializationOptions-sourced config should override an on-disk default.
auto from_json =
Config::load_from_json(R"({ "project": { "max_active_file": 42 } })", "/workspace");
EXPECT_TRUE(from_json.has_value());
EXPECT_EQ(from_json->project.max_active_file.value, 42);
// Unset fields still receive defaults.
EXPECT_EQ(*from_json->project.enable_indexing, true);
EXPECT_EQ(from_json->project.stateful_worker_count.value, 2u);
}
TEST_CASE(XdgHashUnique) {
// Different workspace roots must map to different cache dirs,
// same workspace root must map to the same dir (deterministic).
TempDir tmp;
auto cache_base = tmp.path("xdg");
set_env("XDG_CACHE_HOME", cache_base.c_str());
Config a, b, c;
a.apply_defaults("/ws/project-a");
b.apply_defaults("/ws/project-b");
c.apply_defaults("/ws/project-a");
unset_env("XDG_CACHE_HOME");
EXPECT_NE(std::string_view(a.project.cache_dir), std::string_view(b.project.cache_dir));
EXPECT_EQ(std::string_view(a.project.cache_dir), std::string_view(c.project.cache_dir));
}
TEST_CASE(HomeFallback) {
// With XDG_CACHE_HOME unset but HOME set, cache dir should be under $HOME/.cache/clice.
TempDir tmp;
unset_env("XDG_CACHE_HOME");
auto home = tmp.path("home");
// Save prior value so we restore cleanly.
const char* prior = std::getenv("HOME");
std::string prior_home = prior ? prior : "";
set_env("HOME", home.c_str());
Config config;
config.apply_defaults("/some/ws");
if(prior_home.empty())
unset_env("HOME");
else
set_env("HOME", prior_home.c_str());
std::string cache = path::convert_to_slash(std::string_view(config.project.cache_dir));
std::string home_posix = path::convert_to_slash(home);
EXPECT_TRUE(llvm::StringRef(cache).starts_with(home_posix + "/.cache/clice/"));
}
TEST_CASE(WorkspaceCacheFallback) {
// No XDG, no HOME → should fall back to ${workspace}/.clice.
unset_env("XDG_CACHE_HOME");
const char* prior = std::getenv("HOME");
std::string prior_home = prior ? prior : "";
unset_env("HOME");
Config config;
config.apply_defaults("/ws/root");
if(!prior_home.empty())
set_env("HOME", prior_home.c_str());
EXPECT_EQ(path::convert_to_slash(std::string_view(config.project.cache_dir)),
"/ws/root/.clice");
EXPECT_EQ(path::convert_to_slash(std::string_view(config.project.index_dir)),
"/ws/root/.clice/index");
EXPECT_EQ(path::convert_to_slash(std::string_view(config.project.logging_dir)),
"/ws/root/.clice/logs");
}
TEST_CASE(WorkspaceSubstEmpty) {
// Empty workspace_root must not rewrite "${workspace}" into "" and produce
// bogus paths like "/cache" — the placeholder should be left intact.
Config config;
config.project.cache_dir = "${workspace}/cache";
config.apply_defaults("");
EXPECT_EQ(std::string_view(config.project.cache_dir), "${workspace}/cache");
}
TEST_CASE(WorkspaceSubstRepeated) {
// Multiple ${workspace} occurrences in one string all get substituted.
Config config;
config.project.cache_dir = "${workspace}/a/${workspace}/b";
config.apply_defaults("/root");
EXPECT_EQ(std::string_view(config.project.cache_dir), "/root/a//root/b");
}
TEST_CASE(CompilePathsList) {
// compile_commands_paths should substitute ${workspace} on every entry.
Config config;
config.project.compile_commands_paths = {
"${workspace}/build",
"/abs/path/compile_commands.json",
"${workspace}/out",
};
config.apply_defaults("/ws");
EXPECT_EQ(config.project.compile_commands_paths.size(), 3u);
EXPECT_EQ(config.project.compile_commands_paths[0], "/ws/build");
EXPECT_EQ(config.project.compile_commands_paths[1], "/abs/path/compile_commands.json");
EXPECT_EQ(config.project.compile_commands_paths[2], "/ws/out");
}
TEST_CASE(TomlErrorLocated) {
// Malformed TOML (bad table header, missing close-bracket) must return nullopt.
TempDir tmp;
tmp.touch("clice.toml", "[project\nclang_tidy = true\n");
auto result = Config::load(tmp.path("clice.toml"), tmp.root.str());
EXPECT_FALSE(result.has_value());
}
TEST_CASE(WorkspaceMalformedFallback) {
// load_from_workspace must fall back to defaults when clice.toml is malformed,
// not propagate the failure.
TempDir tmp;
tmp.touch("clice.toml", "[project\ninvalid");
auto config = Config::load_from_workspace(tmp.root.str());
// Defaults still applied.
EXPECT_EQ(config.project.stateful_worker_count.value, 2u);
EXPECT_EQ(*config.project.enable_indexing, true);
}
TEST_CASE(RuleOrderLaterRemoveWins) {
// Later rule's `remove` must cancel an earlier rule's matching `append`.
Config config;
config.rules.push_back(ConfigRule{
.patterns = {"**/*.cpp"},
.append = {"-DFOO", "-DBAR"},
});
config.rules.push_back(ConfigRule{
.patterns = {"**/*.cpp"},
.remove = {"-DFOO"},
});
config.apply_defaults("");
std::vector<std::string> append, remove;
config.match_rules("/src/a.cpp", append, remove);
// -DFOO should have been stripped from append; -DBAR remains.
EXPECT_EQ(append.size(), 1u);
EXPECT_EQ(append[0], "-DBAR");
// remove is still forwarded so base CDB flags also get filtered.
EXPECT_EQ(remove.size(), 1u);
EXPECT_EQ(remove[0], "-DFOO");
}
TEST_CASE(RuleOrderLaterAppendWins) {
// Later append comes after earlier append — at compiler level, last wins
// for flags like -O; verify the ordering is preserved.
Config config;
config.rules.push_back(ConfigRule{
.patterns = {"**/*.cpp"},
.append = {"-O2"},
});
config.rules.push_back(ConfigRule{
.patterns = {"**/*.cpp"},
.append = {"-O3"},
});
config.apply_defaults("");
std::vector<std::string> append, remove;
config.match_rules("/src/a.cpp", append, remove);
EXPECT_EQ(append.size(), 2u);
EXPECT_EQ(append[0], "-O2");
EXPECT_EQ(append[1], "-O3");
}
TEST_CASE(InitOptionsOverlayPreservesToml) {
// Mirror the master_server flow: load workspace config from clice.toml first,
// then overlay initializationOptions JSON. Fields absent in the JSON must
// keep their clice.toml values; fields present in the JSON override.
TempDir tmp;
tmp.touch("clice.toml", R"(
[project]
cache_dir = "/from/toml"
clang_tidy = true
max_active_file = 16
[[rules]]
patterns = ["**/*.cpp"]
append = ["-DFROM_TOML"]
)");
auto config = Config::load_from_workspace(tmp.root.str());
EXPECT_EQ(std::string_view(config.project.cache_dir), "/from/toml");
EXPECT_EQ(config.project.clang_tidy.value, true);
EXPECT_EQ(config.project.max_active_file.value, 16);
EXPECT_EQ(config.compiled_rules.size(), 1u);
// Overlay only `max_active_file` via JSON.
auto ov = kota::codec::json::parse(R"({ "project": { "max_active_file": 99 } })", config);
EXPECT_TRUE(ov.has_value());
config.apply_defaults(tmp.root.str());
// Overridden field.
EXPECT_EQ(config.project.max_active_file.value, 99);
// Untouched fields stay at TOML values.
EXPECT_EQ(std::string_view(config.project.cache_dir), "/from/toml");
EXPECT_EQ(config.project.clang_tidy.value, true);
// Rules from clice.toml must survive the overlay.
EXPECT_EQ(config.rules.size(), 1u);
EXPECT_EQ(config.compiled_rules.size(), 1u);
EXPECT_EQ(config.rules[0].append[0], "-DFROM_TOML");
}
TEST_CASE(InitOptionsOverlayRulesReplace) {
// When `rules` is present in the overlay JSON, it replaces the whole array
// (kotatsu deserializes the vector by value). `compiled_rules` must be
// rebuilt after apply_defaults so stale compiled entries don't linger.
TempDir tmp;
tmp.touch("clice.toml", R"(
[[rules]]
patterns = ["**/*.cpp"]
append = ["-DTOML_ONLY"]
)");
auto config = Config::load_from_workspace(tmp.root.str());
EXPECT_EQ(config.compiled_rules.size(), 1u);
auto ov = kota::codec::json::parse(
R"({ "rules": [ { "patterns": ["**/*.cc"], "append": ["-DFROM_JSON"] } ] })",
config);
EXPECT_TRUE(ov.has_value());
config.apply_defaults(tmp.root.str());
EXPECT_EQ(config.rules.size(), 1u);
EXPECT_EQ(config.rules[0].append[0], "-DFROM_JSON");
EXPECT_EQ(config.compiled_rules.size(), 1u);
// Original TOML rule no longer applies.
std::vector<std::string> append, remove;
config.match_rules("/src/x.cpp", append, remove);
EXPECT_TRUE(append.empty());
config.match_rules("/src/x.cc", append, remove);
EXPECT_EQ(append.size(), 1u);
EXPECT_EQ(append[0], "-DFROM_JSON");
}
}; // TEST_SUITE(Config)
} // namespace clice::testing

View File

@@ -9,6 +9,8 @@ namespace clice::testing {
namespace {
namespace et = eventide;
// ============================================================================
// End-to-end module compilation through real workers:
// 1. Stateless worker builds PCM for module interface
@@ -36,7 +38,7 @@ TEST_CASE(BuildPCMThenCompileWithImport) {
std::string pcm_path;
bool phase1_done = false;
sl.run([&]() -> kota::task<> {
sl.run([&]() -> et::task<> {
worker::BuildParams params;
params.kind = worker::BuildKind::BuildPCM;
params.file = iface;
@@ -69,7 +71,7 @@ TEST_CASE(BuildPCMThenCompileWithImport) {
bool phase2_done = false;
sf.run([&]() -> kota::task<> {
sf.run([&]() -> et::task<> {
worker::CompileParams params;
params.path = consumer;
params.version = 1;
@@ -121,7 +123,7 @@ TEST_CASE(BuildPCMChainThenCompile) {
std::string pcm_a, pcm_b;
bool pcm_done = false;
sl.run([&]() -> kota::task<> {
sl.run([&]() -> et::task<> {
// Build PCM for A first.
{
worker::BuildParams params;
@@ -177,7 +179,7 @@ TEST_CASE(BuildPCMChainThenCompile) {
bool compile_done = false;
sf.run([&]() -> kota::task<> {
sf.run([&]() -> et::task<> {
worker::CompileParams params;
params.path = consumer;
params.version = 1;
@@ -225,7 +227,7 @@ TEST_CASE(ModuleImplementationUnitWithWorker) {
std::string pcm_path;
bool pcm_done = false;
sl.run([&]() -> kota::task<> {
sl.run([&]() -> et::task<> {
worker::BuildParams params;
params.kind = worker::BuildKind::BuildPCM;
params.file = iface;
@@ -255,7 +257,7 @@ TEST_CASE(ModuleImplementationUnitWithWorker) {
bool compile_done = false;
sf.run([&]() -> kota::task<> {
sf.run([&]() -> et::task<> {
worker::CompileParams params;
params.path = impl;
params.version = 1;

View File

@@ -10,6 +10,8 @@ namespace clice::testing {
namespace {
namespace et = eventide;
// ============================================================================
// End-to-end PCH compilation through real workers:
// 1. Stateless worker builds PCH for preamble headers
@@ -37,7 +39,7 @@ TEST_CASE(BuildPCHThenCompile) {
std::string pch_path;
bool phase1_done = false;
sl.run([&]() -> kota::task<> {
sl.run([&]() -> et::task<> {
worker::BuildParams params;
params.kind = worker::BuildKind::BuildPCH;
params.file = main_file;
@@ -77,7 +79,7 @@ TEST_CASE(BuildPCHThenCompile) {
auto preamble_bound = compute_preamble_bound(main_text);
sf.run([&]() -> kota::task<> {
sf.run([&]() -> et::task<> {
worker::CompileParams params;
params.path = main_file;
params.version = 1;
@@ -121,7 +123,7 @@ TEST_CASE(CompileWithoutPCHStillWorks) {
bool compile_done = false;
sf.run([&]() -> kota::task<> {
sf.run([&]() -> et::task<> {
worker::CompileParams params;
params.path = main_file;
params.version = 1;

View File

@@ -2,15 +2,16 @@
#include <vector>
#include "test/test.h"
#include "eventide/serde/serde/raw_value.h"
#include "server/protocol.h"
#include "server/worker_test_helpers.h"
#include "kota/codec/json/json.h"
namespace clice::testing {
namespace {
namespace et = eventide;
TEST_SUITE(StatefulWorker) {
TEST_CASE(SpawnAndExit) {
@@ -32,7 +33,7 @@ TEST_CASE(CompileRequest) {
bool test_done = false;
w.run([&]() -> kota::task<> {
w.run([&]() -> et::task<> {
worker::CompileParams params;
params.path = src;
params.version = 1;
@@ -58,7 +59,7 @@ TEST_CASE(HoverWithoutCompile) {
bool test_done = false;
w.run([&]() -> kota::task<> {
w.run([&]() -> et::task<> {
// Hover on a file that hasn't been compiled should return null.
worker::QueryParams params;
params.kind = worker::QueryKind::Hover;
@@ -87,7 +88,7 @@ TEST_CASE(CompileThenHover) {
bool test_done = false;
w.run([&]() -> kota::task<> {
w.run([&]() -> et::task<> {
// First compile
worker::CompileParams cp;
cp.path = src;
@@ -128,7 +129,7 @@ TEST_CASE(DocumentUpdate) {
bool test_done = false;
w.run([&]() -> kota::task<> {
w.run([&]() -> et::task<> {
// Compile first
worker::CompileParams cp;
cp.path = src;
@@ -169,7 +170,7 @@ TEST_CASE(CodeActionReturnsEmpty) {
bool test_done = false;
w.run([&]() -> kota::task<> {
w.run([&]() -> et::task<> {
worker::QueryParams params;
params.kind = worker::QueryKind::CodeAction;
params.path = "/tmp/test.cpp";
@@ -191,7 +192,7 @@ TEST_CASE(GoToDefinitionReturnsEmpty) {
bool test_done = false;
w.run([&]() -> kota::task<> {
w.run([&]() -> et::task<> {
worker::QueryParams params;
params.kind = worker::QueryKind::GoToDefinition;
params.path = "/tmp/test.cpp";
@@ -214,7 +215,7 @@ TEST_CASE(SemanticTokensWithoutCompile) {
bool test_done = false;
w.run([&]() -> kota::task<> {
w.run([&]() -> et::task<> {
worker::QueryParams params;
params.kind = worker::QueryKind::SemanticTokens;
params.path = "/tmp/nonexistent.cpp";
@@ -235,7 +236,7 @@ TEST_CASE(FoldingRangeWithoutCompile) {
bool test_done = false;
w.run([&]() -> kota::task<> {
w.run([&]() -> et::task<> {
worker::QueryParams params;
params.kind = worker::QueryKind::FoldingRange;
params.path = "/tmp/nonexistent.cpp";
@@ -256,7 +257,7 @@ TEST_CASE(DocumentSymbolWithoutCompile) {
bool test_done = false;
w.run([&]() -> kota::task<> {
w.run([&]() -> et::task<> {
worker::QueryParams params;
params.kind = worker::QueryKind::DocumentSymbol;
params.path = "/tmp/nonexistent.cpp";
@@ -277,7 +278,7 @@ TEST_CASE(DocumentLinkWithoutCompile) {
bool test_done = false;
w.run([&]() -> kota::task<> {
w.run([&]() -> et::task<> {
worker::QueryParams params;
params.kind = worker::QueryKind::DocumentLink;
params.path = "/tmp/nonexistent.cpp";
@@ -298,7 +299,7 @@ TEST_CASE(InlayHintsWithoutCompile) {
bool test_done = false;
w.run([&]() -> kota::task<> {
w.run([&]() -> et::task<> {
worker::QueryParams params;
params.kind = worker::QueryKind::InlayHints;
params.path = "/tmp/nonexistent.cpp";
@@ -329,7 +330,7 @@ TEST_CASE(MultipleSequentialRequests) {
bool test_done = false;
w.run([&]() -> kota::task<> {
w.run([&]() -> et::task<> {
// Compile first so feature requests return real data.
worker::CompileParams cp;
cp.path = src;
@@ -401,7 +402,7 @@ TEST_CASE(MultipleDocuments) {
bool test_done = false;
w.run([&]() -> kota::task<> {
w.run([&]() -> et::task<> {
// Compile 3 different documents.
for(int i = 0; i < 3; i++) {
worker::CompileParams cp;
@@ -439,7 +440,7 @@ TEST_CASE(EvictNotification) {
bool test_done = false;
w.run([&]() -> kota::task<> {
w.run([&]() -> et::task<> {
// Send an evict notification — worker should remove the document without crashing.
worker::EvictParams ep;
ep.path = "/tmp/evict_test.cpp";
@@ -473,7 +474,7 @@ TEST_CASE(SpawnWithMemoryLimit) {
bool test_done = false;
w.run([&]() -> kota::task<> {
w.run([&]() -> et::task<> {
// Compile first.
worker::CompileParams cp;
cp.path = src;

View File

@@ -2,15 +2,17 @@
#include <vector>
#include "test/test.h"
#include "eventide/serde/bincode/bincode.h"
#include "eventide/serde/serde/raw_value.h"
#include "server/protocol.h"
#include "server/worker_test_helpers.h"
#include "kota/codec/bincode/bincode.h"
namespace clice::testing {
namespace {
namespace et = eventide;
// ============================================================================
// Bincode Serialization Tests
// ============================================================================
@@ -18,7 +20,7 @@ namespace {
TEST_SUITE(BincodeRoundTrip) {
TEST_CASE(CompileParamsRoundTrip) {
namespace bincode = kota::codec::bincode;
namespace bincode = eventide::serde::bincode;
worker::CompileParams params;
params.path = "/tmp/test.cpp";
@@ -45,7 +47,7 @@ TEST_CASE(CompileParamsRoundTrip) {
}
TEST_CASE(CompileResultRoundTrip) {
namespace bincode = kota::codec::bincode;
namespace bincode = eventide::serde::bincode;
worker::CompileResult result;
result.version = 1;
@@ -90,7 +92,7 @@ TEST_CASE(BuildPCHRequest) {
bool test_done = false;
w.run([&]() -> kota::task<> {
w.run([&]() -> et::task<> {
worker::BuildParams params;
params.kind = worker::BuildKind::BuildPCH;
params.file = hdr;
@@ -125,7 +127,7 @@ TEST_CASE(IndexRequest) {
bool test_done = false;
w.run([&]() -> kota::task<> {
w.run([&]() -> et::task<> {
worker::BuildParams params;
params.kind = worker::BuildKind::Index;
params.file = src;
@@ -160,7 +162,7 @@ TEST_CASE(BuildPCMRequest) {
bool test_done = false;
w.run([&]() -> kota::task<> {
w.run([&]() -> et::task<> {
worker::BuildParams params;
params.kind = worker::BuildKind::BuildPCM;
params.file = src;
@@ -194,7 +196,7 @@ TEST_CASE(CompletionRequest) {
bool test_done = false;
w.run([&]() -> kota::task<> {
w.run([&]() -> et::task<> {
worker::BuildParams params;
params.kind = worker::BuildKind::Completion;
params.file = src;
@@ -224,7 +226,7 @@ TEST_CASE(SignatureHelpRequest) {
bool test_done = false;
w.run([&]() -> kota::task<> {
w.run([&]() -> et::task<> {
worker::BuildParams params;
params.kind = worker::BuildKind::SignatureHelp;
params.file = src;
@@ -259,7 +261,7 @@ TEST_CASE(MultipleStatelessRequests) {
bool test_done = false;
w.run([&]() -> kota::task<> {
w.run([&]() -> et::task<> {
// Send multiple index requests to test stateless worker handles them sequentially.
for(int i = 0; i < 3; i++) {
worker::BuildParams params;

View File

@@ -11,14 +11,12 @@
#include "test/temp_dir.h"
#include "command/argument_parser.h"
#include "command/command.h"
#include "eventide/async/async.h"
#include "eventide/ipc/peer.h"
#include "eventide/ipc/transport.h"
#include "server/protocol.h"
#include "support/filesystem.h"
#include "kota/async/async.h"
#include "kota/ipc/codec/bincode.h"
#include "kota/ipc/peer.h"
#include "kota/ipc/transport.h"
namespace clice::testing {
namespace {
@@ -34,6 +32,8 @@ struct SigpipeGuard {
static SigpipeGuard sigpipe_guard;
namespace et = eventide;
/// Resolve path to the clice binary for spawning workers.
inline std::string clice_binary() {
auto res_dir = resource_dir();
@@ -59,10 +59,10 @@ inline std::vector<std::string> make_args(const std::string& file_path,
/// Helper: spawn a worker process and return a BincodePeer connected to it.
struct WorkerHandle {
kota::event_loop loop;
kota::process proc{};
std::unique_ptr<kota::ipc::StreamTransport> transport;
std::unique_ptr<kota::ipc::BincodePeer> peer;
et::event_loop loop;
et::process proc{};
std::unique_ptr<et::ipc::StreamTransport> transport;
std::unique_ptr<et::ipc::BincodePeer> peer;
int stderr_fd = -1;
bool spawn(const std::string& mode, std::uint64_t memory_limit = 0) {
@@ -74,7 +74,7 @@ struct WorkerHandle {
stderr_fd = ::open(stderr_path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
#endif
kota::process::options opts;
et::process::options opts;
opts.file = binary;
opts.args = {binary, "--mode", mode};
if(memory_limit > 0) {
@@ -82,13 +82,12 @@ struct WorkerHandle {
opts.args.push_back(std::to_string(memory_limit));
}
opts.streams = {
kota::process::stdio::pipe(true, false), // stdin: child reads
kota::process::stdio::pipe(false, true), // stdout: child writes
stderr_fd >= 0 ? kota::process::stdio::from_fd(stderr_fd)
: kota::process::stdio::ignore(),
et::process::stdio::pipe(true, false), // stdin: child reads
et::process::stdio::pipe(false, true), // stdout: child writes
stderr_fd >= 0 ? et::process::stdio::from_fd(stderr_fd) : et::process::stdio::ignore(),
};
auto result = kota::process::spawn(opts, loop);
auto result = et::process::spawn(opts, loop);
if(!result) {
#ifndef _WIN32
if(stderr_fd >= 0)
@@ -98,9 +97,9 @@ struct WorkerHandle {
}
auto& spawn = *result;
transport = std::make_unique<kota::ipc::StreamTransport>(std::move(spawn.stdout_pipe),
std::move(spawn.stdin_pipe));
peer = std::make_unique<kota::ipc::BincodePeer>(loop, std::move(transport));
transport = std::make_unique<et::ipc::StreamTransport>(std::move(spawn.stdout_pipe),
std::move(spawn.stdin_pipe));
peer = std::make_unique<et::ipc::BincodePeer>(loop, std::move(transport));
proc = std::move(spawn.proc);
#ifndef _WIN32
if(stderr_fd >= 0)

View File

@@ -83,87 +83,5 @@ int x = 1;
}
}; // TEST_SUITE(SourceText)
TEST_SUITE(DirectiveArgument) {
void EXPECT_RANGE(llvm::StringRef content, std::uint32_t offset, llvm::StringRef expected) {
auto result = find_directive_argument(content, offset);
ASSERT_TRUE(result.has_value());
ASSERT_EQ(content.substr(result->begin, result->length()), expected);
}
void EXPECT_NONE(llvm::StringRef content, std::uint32_t offset) {
auto result = find_directive_argument(content, offset);
ASSERT_FALSE(result.has_value());
}
TEST_CASE(IncludeQuoted) {
llvm::StringRef src = R"(#include "foo.h")";
EXPECT_RANGE(src, 0, R"("foo.h")");
}
TEST_CASE(IncludeAngled) {
llvm::StringRef src = "#include <iostream>";
EXPECT_RANGE(src, 0, "<iostream>");
}
TEST_CASE(IncludeMacro) {
llvm::StringRef src = "#include HEADER";
EXPECT_RANGE(src, 0, "HEADER");
}
TEST_CASE(HasIncludeQuoted) {
llvm::StringRef src = R"(#if __has_include("foo.h"))";
// offset at __has_include
auto pos = src.find("__has_include");
EXPECT_RANGE(src, static_cast<std::uint32_t>(pos), R"("foo.h")");
}
TEST_CASE(HasIncludeAngled) {
llvm::StringRef src = "#if __has_include(<vector>)";
auto pos = src.find("__has_include");
EXPECT_RANGE(src, static_cast<std::uint32_t>(pos), "<vector>");
}
TEST_CASE(EmbedQuoted) {
llvm::StringRef src = R"(#embed "data.bin")";
EXPECT_RANGE(src, 0, R"("data.bin")");
}
TEST_CASE(HasEmbedQuoted) {
llvm::StringRef src = R"(#if __has_embed("data.bin"))";
auto pos = src.find("__has_embed");
EXPECT_RANGE(src, static_cast<std::uint32_t>(pos), R"("data.bin")");
}
TEST_CASE(MultilineOffset) {
llvm::StringRef src = "#include \"a.h\"\n#include \"b.h\"";
// offset pointing into the second line
auto pos = src.find("#include \"b.h\"");
EXPECT_RANGE(src, static_cast<std::uint32_t>(pos), R"("b.h")");
}
TEST_CASE(EmptyDirective) {
llvm::StringRef src = "#include \n";
EXPECT_NONE(src, 0);
}
TEST_CASE(HasIncludeFromLineStart) {
llvm::StringRef src = "#if __has_include(<vector>)";
EXPECT_RANGE(src, 0, "<vector>");
}
TEST_CASE(HasEmbedFromLineStart) {
llvm::StringRef src = R"(#if __has_embed("data.bin"))";
EXPECT_RANGE(src, 0, R"("data.bin")");
}
TEST_CASE(IncludeNext) {
llvm::StringRef src = "#include_next <stdlib.h>";
EXPECT_RANGE(src, 0, "<stdlib.h>");
}
}; // TEST_SUITE(DirectiveArgument)
} // namespace
} // namespace clice::testing

View File

@@ -6,6 +6,5 @@
#include <vector>
#include "test/platform.h"
#include "eventide/zest/macro.h"
#include "support/format.h"
#include "kota/zest/macro.h"

View File

@@ -7,44 +7,6 @@
namespace clice::testing {
namespace {
std::vector<std::string> base_cc1_args(llvm::StringRef standard) {
return {
"clang",
"-cc1",
"-triple",
LLVM_DEFAULT_TARGET_TRIPLE,
standard.str(),
"-ffreestanding",
"-undef",
"-fms-extensions",
"-fsyntax-only",
"-x",
"c++",
};
}
} // namespace
Tester::~Tester() {
for(auto& path: pcm_paths) {
fs::remove(path);
}
}
bool Tester::try_compile() {
auto built = clice::compile(params);
if(!built.completed()) {
for(auto& diag: built.diagnostics()) {
LOG_ERROR("{}", diag.message);
}
return false;
}
unit.emplace(std::move(built));
return true;
}
void Tester::prepare(llvm::StringRef standard) {
params = CompilationParams();
unit.reset();
@@ -54,7 +16,19 @@ void Tester::prepare(llvm::StringRef standard) {
vfs->add(file, source.content);
}
owned_args = base_cc1_args(standard);
owned_args.clear();
// Use -cc1 mode directly to bypass the slow driver subprocess.
owned_args.push_back("clang");
owned_args.push_back("-cc1");
owned_args.push_back("-triple");
owned_args.push_back(LLVM_DEFAULT_TARGET_TRIPLE);
owned_args.push_back(standard.str());
owned_args.push_back("-ffreestanding");
owned_args.push_back("-undef");
owned_args.push_back("-fms-extensions");
owned_args.push_back("-fsyntax-only");
owned_args.push_back("-x");
owned_args.push_back("c++");
owned_args.push_back(TestVFS::path(src_path));
params.arguments.clear();
@@ -68,7 +42,17 @@ void Tester::prepare(llvm::StringRef standard) {
bool Tester::compile(llvm::StringRef standard) {
prepare(standard);
return try_compile();
auto built = clice::compile(params);
if(!built.completed()) {
for(auto& diag: built.diagnostics()) {
LOG_ERROR("{}", diag.message);
}
return false;
}
unit.emplace(std::move(built));
return true;
}
bool Tester::compile_with_pch(llvm::StringRef standard) {
@@ -80,6 +64,7 @@ bool Tester::compile_with_pch(llvm::StringRef standard) {
return false;
}
// Use an overlay VFS so the PCH temp file on real disk is accessible.
auto overlay =
llvm::makeIntrusiveRefCnt<llvm::vfs::OverlayFileSystem>(llvm::vfs::getRealFileSystem());
overlay->pushOverlay(vfs);
@@ -111,123 +96,16 @@ bool Tester::compile_with_pch(llvm::StringRef standard) {
params.pch = {info.path, static_cast<std::uint32_t>(info.preamble.size())};
params.buffers.clear();
return try_compile();
}
bool Tester::compile_with_modules(llvm::StringRef standard) {
std::vector<ModuleFile> all_modules = module_files;
for(auto& [file, source]: sources.all_files) {
if(file == src_path) {
continue;
}
auto result = scan(source.content);
if(!result.module_name.empty() || result.need_preprocess) {
all_modules.push_back({file.str(), source.content});
auto built = clice::compile(params);
if(!built.completed()) {
for(auto& diag: built.diagnostics()) {
LOG_ERROR("{}", diag.message);
}
return false;
}
if(all_modules.empty()) {
return compile(standard);
}
vfs = llvm::makeIntrusiveRefCnt<TestVFS>();
for(auto& [file, source]: sources.all_files) {
vfs->add(file, source.content);
}
for(auto& mod: module_files) {
vfs->add(mod.filename, mod.content);
}
struct ScannedModule {
std::string filename;
std::string content;
std::string module_name;
std::vector<std::string> deps;
};
auto scan_args_base = base_cc1_args(standard);
std::vector<ScannedModule> modules;
for(auto& mod: all_modules) {
auto args = scan_args_base;
args.push_back(TestVFS::path(mod.filename));
std::vector<const char*> argv;
for(auto& arg: args) {
argv.push_back(arg.c_str());
}
auto result = scan_precise(argv, TestVFS::root(), {}, nullptr, vfs);
modules.push_back(
{mod.filename, mod.content, result.module_name, std::move(result.modules)});
}
llvm::StringMap<std::size_t> name_to_index;
for(std::size_t i = 0; i < modules.size(); ++i) {
name_to_index[modules[i].module_name] = i;
}
std::vector<std::size_t> order;
std::vector<int> state(modules.size(), 0);
auto topo_visit = [&](this auto& self, std::size_t i) -> bool {
if(state[i] == 2)
return true;
if(state[i] == 1) {
LOG_ERROR("Circular module dependency involving {}", modules[i].module_name);
return false;
}
state[i] = 1;
for(auto& dep: modules[i].deps) {
auto it = name_to_index.find(dep);
if(it != name_to_index.end()) {
if(!self(it->second))
return false;
}
}
state[i] = 2;
order.push_back(i);
return true;
};
for(std::size_t i = 0; i < modules.size(); ++i) {
if(!topo_visit(i))
return false;
}
auto overlay =
llvm::makeIntrusiveRefCnt<llvm::vfs::OverlayFileSystem>(llvm::vfs::getRealFileSystem());
overlay->pushOverlay(vfs);
llvm::StringMap<std::string> built_pcms;
for(auto idx: order) {
auto& mod = modules[idx];
auto pcm_path = fs::createTemporaryFile("clice", "pcm");
if(!pcm_path) {
LOG_ERROR("{}", pcm_path.error().message());
return false;
}
pcm_paths.push_back(*pcm_path);
Tester builder;
builder.add_main(mod.filename, mod.content);
builder.prepare(standard);
builder.params.kind = CompilationKind::ModuleInterface;
builder.params.output_file = *pcm_path;
builder.params.vfs = overlay;
builder.params.pcms = built_pcms;
if(!builder.try_compile())
return false;
built_pcms.try_emplace(mod.module_name, *pcm_path);
}
prepare(standard);
params.vfs = overlay;
params.pcms = std::move(built_pcms);
return try_compile();
unit.emplace(std::move(built));
return true;
}
std::uint32_t Tester::point(llvm::StringRef name, llvm::StringRef file) {
@@ -288,11 +166,13 @@ void Tester::prepare_driver(llvm::StringRef standard) {
params.kind = CompilationKind::Content;
// Use overlay VFS: real FS (for system headers) + InMemoryFS (for test files).
auto overlay =
llvm::makeIntrusiveRefCnt<llvm::vfs::OverlayFileSystem>(llvm::vfs::getRealFileSystem());
overlay->pushOverlay(vfs);
params.vfs = overlay;
// Remap test files so clang sees our in-memory content.
for(auto& [file, source]: sources.all_files) {
if(file == src_path) {
params.add_remapped_file(file, source.content);
@@ -305,11 +185,36 @@ void Tester::prepare_driver(llvm::StringRef standard) {
bool Tester::compile_driver(llvm::StringRef standard) {
prepare_driver(standard);
return try_compile();
auto built = clice::compile(params);
if(!built.completed()) {
for(auto& diag: built.diagnostics()) {
LOG_ERROR("{}", diag.message);
}
return false;
}
unit.emplace(std::move(built));
return true;
}
bool Tester::compile_driver_with_pch(llvm::StringRef standard) {
prepare_driver(standard);
params = CompilationParams();
unit.reset();
vfs = llvm::makeIntrusiveRefCnt<TestVFS>();
for(auto& [file, source]: sources.all_files) {
vfs->add(file, source.content);
}
auto command = std::format("clang++ {} {} -fms-extensions", standard, src_path);
database.add_command("fake", src_path, command);
CommandOptions options;
options.query_toolchain = true;
options.suppress_logging = true;
auto commands = database.lookup(src_path, options);
assert(!commands.empty() && "lookup failed after add_command");
params.arguments = commands.front().to_argv();
auto pch_path = fs::createTemporaryFile("clice", "pch");
if(!pch_path) {
@@ -317,12 +222,16 @@ bool Tester::compile_driver_with_pch(llvm::StringRef standard) {
return false;
}
// Use overlay VFS: real FS (for system headers + PCH temp) + InMemoryFS.
auto overlay =
llvm::makeIntrusiveRefCnt<llvm::vfs::OverlayFileSystem>(llvm::vfs::getRealFileSystem());
overlay->pushOverlay(vfs);
params.vfs = overlay;
// Phase 1: Build PCH from the preamble portion.
params.kind = CompilationKind::Preamble;
params.output_file = *pch_path;
// Clear buffers from prepare_driver() so we can re-add with preamble bound.
params.buffers.clear();
for(auto& [file, source]: sources.all_files) {
if(file == src_path) {
auto bound = compute_preamble_bound(source.content);
@@ -350,7 +259,25 @@ bool Tester::compile_driver_with_pch(llvm::StringRef standard) {
params.pch = {info.path, static_cast<std::uint32_t>(info.preamble.size())};
params.buffers.clear();
return try_compile();
for(auto& [file, source]: sources.all_files) {
if(file == src_path) {
params.add_remapped_file(file, source.content);
} else {
std::string path = path::is_absolute(file) ? file.str() : path::join(".", file);
params.add_remapped_file(path, source.content);
}
}
auto built = clice::compile(params);
if(!built.completed()) {
for(auto& diag: built.diagnostics()) {
LOG_ERROR("{}", diag.message);
}
return false;
}
unit.emplace(std::move(built));
return true;
}
void Tester::clear() {
@@ -361,11 +288,6 @@ void Tester::clear() {
src_path.clear();
owned_args.clear();
vfs.reset();
module_files.clear();
for(auto& path: pcm_paths) {
fs::remove(path);
}
pcm_paths.clear();
}
} // namespace clice::testing

View File

@@ -2,13 +2,11 @@
#include <optional>
#include <string>
#include <vector>
#include "test/annotation.h"
#include "test/test.h"
#include "command/command.h"
#include "compile/compilation.h"
#include "feature/feature.h"
#include "support/logging.h"
namespace clice::testing {
@@ -27,16 +25,6 @@ struct Tester {
/// The VFS used for compilation.
llvm::IntrusiveRefCntPtr<TestVFS> vfs;
struct ModuleFile {
std::string filename;
std::string content;
};
std::vector<ModuleFile> module_files;
std::vector<std::string> pcm_paths;
~Tester();
void add_main(llvm::StringRef file, llvm::StringRef content) {
src_path = file.str();
sources.add_source(file, content);
@@ -51,10 +39,6 @@ struct Tester {
sources.add_sources(content);
}
void add_module(llvm::StringRef filename, llvm::StringRef content) {
module_files.push_back({filename.str(), content.str()});
}
/// Fast VFS-only path: uses -cc1 directly, no system headers.
void prepare(llvm::StringRef standard = "-std=c++20");
@@ -62,8 +46,6 @@ struct Tester {
bool compile_with_pch(llvm::StringRef standard = "-std=c++20");
bool compile_with_modules(llvm::StringRef standard = "-std=c++20");
/// Driver path: uses CompilationDatabase + toolchain cache, has system headers.
void prepare_driver(llvm::StringRef standard = "-std=c++20");
@@ -71,8 +53,6 @@ struct Tester {
bool compile_driver_with_pch(llvm::StringRef standard = "-std=c++20");
bool try_compile();
std::uint32_t operator[](llvm::StringRef file, llvm::StringRef pos) {
return sources.all_files.lookup(file).offsets.lookup(pos);
}
@@ -83,12 +63,6 @@ struct Tester {
LocalSourceRange range(llvm::StringRef name = "", llvm::StringRef file = "");
LocalSourceRange to_local_range(const kota::ipc::protocol::Range& range) {
feature::PositionMapper converter(unit->interested_content(),
feature::PositionEncoding::UTF8);
return LocalSourceRange(*converter.to_offset(range.start), *converter.to_offset(range.end));
}
void clear();
};

View File

@@ -1,14 +1,13 @@
#include <string>
#include <string_view>
#include "eventide/deco/deco.h"
#include "eventide/zest/zest.h"
#include "support/logging.h"
#include "kota/deco/deco.h"
#include "kota/zest/zest.h"
namespace {
using kota::deco::decl::KVStyle;
using deco::decl::KVStyle;
struct TestOptions {
DecoKV(style = KVStyle::JoinedOrSeparate,
@@ -33,8 +32,8 @@ struct TestOptions {
} // namespace
int main(int argc, const char** argv) {
auto args = kota::deco::util::argvify(argc, argv);
auto parsed = kota::deco::cli::parse<TestOptions>(args);
auto args = deco::util::argvify(argc, argv);
auto parsed = deco::cli::parse<TestOptions>(args);
std::string_view filter = {};
if(parsed.has_value() && parsed->options.test_filter.has_value()) {
@@ -58,5 +57,5 @@ int main(int argc, const char** argv) {
clice::logging::stderr_logger("test", clice::logging::options);
return kota::zest::Runner::instance().run_tests(filter);
return eventide::zest::Runner::instance().run_tests(filter);
}