Compare commits
11 Commits
feat/docum
...
raw-foldin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
183b90d572 | ||
|
|
939ab6d0d4 | ||
|
|
e1202d2fa5 | ||
|
|
17e68010a0 | ||
|
|
3fa653bcaf | ||
|
|
592b37417e | ||
|
|
418e190fa0 | ||
|
|
d42d9d5b29 | ||
|
|
9c89d20e76 | ||
|
|
8bafaa8171 | ||
|
|
92dae18fd4 |
@@ -100,7 +100,7 @@ SortIncludes: true
|
||||
SortUsingDeclarations: Never
|
||||
IncludeBlocks: Regroup
|
||||
IncludeCategories:
|
||||
- Regex: '^["<](spdlog|toml\+\+|coraing|cpptrace|flatbuffers)/'
|
||||
- Regex: '^["<](spdlog|toml\+\+|coraing|cpptrace|flatbuffers|kota)/'
|
||||
Priority: 30
|
||||
SortPriority: 31
|
||||
|
||||
|
||||
2
.github/actions/setup-pixi/action.yml
vendored
2
.github/actions/setup-pixi/action.yml
vendored
@@ -13,7 +13,7 @@ runs:
|
||||
- name: Setup Pixi
|
||||
uses: prefix-dev/setup-pixi@v0.9.3
|
||||
with:
|
||||
pixi-version: v0.62.0
|
||||
pixi-version: v0.67.0
|
||||
environments: ${{ inputs.environments }}
|
||||
activate-environment: true
|
||||
cache: true
|
||||
|
||||
2
.github/workflows/benchmark.yml
vendored
2
.github/workflows/benchmark.yml
vendored
@@ -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
|
||||
|
||||
345
.github/workflows/build-llvm.yml
vendored
345
.github/workflows/build-llvm.yml
vendored
@@ -1,6 +1,22 @@
|
||||
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.
|
||||
@@ -12,9 +28,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-2025
|
||||
llvm_mode: Debug
|
||||
lto: OFF
|
||||
# Native builds
|
||||
- os: windows-2025
|
||||
llvm_mode: RelWithDebInfo
|
||||
lto: OFF
|
||||
@@ -39,6 +53,42 @@ 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
|
||||
@@ -67,49 +117,91 @@ jobs:
|
||||
free -h
|
||||
df -h
|
||||
|
||||
- name: Setup Pixi
|
||||
uses: prefix-dev/setup-pixi@v0.9.3
|
||||
- uses: ./.github/actions/setup-pixi
|
||||
with:
|
||||
pixi-version: v0.59.0
|
||||
environments: package
|
||||
activate-environment: true
|
||||
cache: true
|
||||
locked: true
|
||||
environments: ${{ matrix.pixi_env || 'package' }}
|
||||
|
||||
- name: Clone llvm-project (21.1.4)
|
||||
- name: Clone llvm-project
|
||||
shell: bash
|
||||
run: |
|
||||
git clone --branch llvmorg-21.1.4 --depth 1 https://github.com/llvm/llvm-project.git .llvm
|
||||
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
|
||||
|
||||
- name: Build LLVM (install-distribution)
|
||||
shell: bash
|
||||
run: |
|
||||
pixi run build-llvm --llvm-src=.llvm --mode="${{ matrix.llvm_mode }}" --lto="${{ matrix.lto }}" --build-dir=build
|
||||
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}
|
||||
|
||||
- name: Build clice using installed LLVM
|
||||
if: ${{ !matrix.target_triple }}
|
||||
shell: bash
|
||||
run: |
|
||||
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
|
||||
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
|
||||
|
||||
- name: Run tests
|
||||
if: ${{ !matrix.target_triple }}
|
||||
shell: bash
|
||||
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}
|
||||
run: pixi run test ${{ matrix.llvm_mode }}
|
||||
|
||||
# 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.llvm_mode == 'Debug' || (matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF')
|
||||
if: (!matrix.target_triple) && (matrix.llvm_mode == 'Debug' || (matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF'))
|
||||
shell: bash
|
||||
run: |
|
||||
MANIFEST="pruned-libs-${{ matrix.os }}.json"
|
||||
@@ -117,13 +209,13 @@ jobs:
|
||||
python3 scripts/prune-llvm-bin.py \
|
||||
--action discover \
|
||||
--install-dir ".llvm/build-install/lib" \
|
||||
--build-dir "build" \
|
||||
--build-dir "build/${{ matrix.llvm_mode }}" \
|
||||
--max-attempts 60 \
|
||||
--sleep-seconds 60 \
|
||||
--manifest "${MANIFEST}"
|
||||
|
||||
- name: Upload pruned-libs manifest
|
||||
if: matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF'
|
||||
if: (!matrix.target_triple) && matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'OFF'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: llvm-pruned-libs-${{ matrix.os }}
|
||||
@@ -131,8 +223,8 @@ jobs:
|
||||
if-no-files-found: error
|
||||
compression-level: 0
|
||||
|
||||
- name: Apply pruned-libs manifest (RelWithDebInfo + LTO)
|
||||
if: matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'ON'
|
||||
- name: Apply pruned-libs manifest (RelWithDebInfo + LTO, native only)
|
||||
if: (!matrix.target_triple) && matrix.llvm_mode == 'RelWithDebInfo' && matrix.lto == 'ON'
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
@@ -142,7 +234,27 @@ jobs:
|
||||
--action apply \
|
||||
--manifest "${MANIFEST}" \
|
||||
--install-dir ".llvm/build-install/lib" \
|
||||
--build-dir "build" \
|
||||
--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 }}" \
|
||||
--gh-run-id "${{ github.run_id }}" \
|
||||
--gh-artifact "llvm-pruned-libs-${{ matrix.os }}" \
|
||||
--gh-download-dir "artifacts" \
|
||||
@@ -157,23 +269,35 @@ jobs:
|
||||
MODE_TAG="debug"
|
||||
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"
|
||||
# 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
|
||||
fi
|
||||
|
||||
SUFFIX=""
|
||||
if [[ "${{ matrix.lto }}" == "ON" ]]; then
|
||||
SUFFIX="-lto"
|
||||
fi
|
||||
if [[ "${{ matrix.llvm_mode }}" == "Debug" ]]; then
|
||||
if [[ "${{ matrix.llvm_mode }}" == "Debug" && "${{ matrix.os }}" != windows-* ]]; then
|
||||
SUFFIX="${SUFFIX}-asan"
|
||||
fi
|
||||
|
||||
@@ -189,3 +313,134 @@ 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
|
||||
|
||||
6
.github/workflows/check-format.yml
vendored
6
.github/workflows/check-format.yml
vendored
@@ -14,6 +14,12 @@ 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
|
||||
|
||||
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@@ -7,6 +7,10 @@ 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/') }}
|
||||
|
||||
39
.github/workflows/publish-clice.yml
vendored
39
.github/workflows/publish-clice.yml
vendored
@@ -9,6 +9,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Native builds
|
||||
- os: windows-2025
|
||||
artifact_name: clice.zip
|
||||
asset_name: clice-x64-windows-msvc.zip
|
||||
@@ -27,6 +28,31 @@ 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:
|
||||
@@ -39,11 +65,20 @@ jobs:
|
||||
|
||||
- uses: ./.github/actions/setup-pixi
|
||||
with:
|
||||
environments: package
|
||||
environments: ${{ matrix.pixi_env || 'package' }}
|
||||
|
||||
- name: Package
|
||||
- name: Package (native)
|
||||
if: ${{ !matrix.target_triple }}
|
||||
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
|
||||
|
||||
131
.github/workflows/test-cmake.yml
vendored
131
.github/workflows/test-cmake.yml
vendored
@@ -17,53 +17,154 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-2025, ubuntu-24.04, macos-15]
|
||||
build_type: [Debug, RelWithDebInfo]
|
||||
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
|
||||
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 }}-ccache-${{ github.sha }}
|
||||
key: ${{ runner.os }}-${{ matrix.build_type }}-${{ matrix.target_triple || 'native' }}-ccache-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ matrix.build_type }}-ccache-
|
||||
${{ runner.os }}-${{ matrix.build_type }}-${{ matrix.target_triple || 'native' }}-ccache-
|
||||
|
||||
- name: Zero cache stats
|
||||
run: |
|
||||
ENV="${{ matrix.pixi_env || 'default' }}"
|
||||
if [ "$RUNNER_OS" = "Windows" ]; then
|
||||
pixi run -- sccache --stop-server || true
|
||||
pixi run -- sccache --zero-stats || true
|
||||
pixi run -e "$ENV" -- sccache --stop-server || true
|
||||
pixi run -e "$ENV" -- sccache --zero-stats || true
|
||||
else
|
||||
pixi run -- ccache --zero-stats || true
|
||||
pixi run -e "$ENV" -- ccache --zero-stats || true
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Build
|
||||
- name: Build (native)
|
||||
if: ${{ !matrix.target_triple }}
|
||||
run: pixi run build ${{ matrix.build_type }} ON
|
||||
|
||||
- name: Unit Test
|
||||
- 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
|
||||
run: pixi run unit-test ${{ matrix.build_type }}
|
||||
|
||||
- name: Integration Test
|
||||
- name: Integration tests
|
||||
if: ${{ !matrix.build_only }}
|
||||
timeout-minutes: 20
|
||||
run: pixi run integration-test ${{ matrix.build_type }}
|
||||
|
||||
- name: Smoke Test
|
||||
if: success() || failure()
|
||||
- name: Smoke tests
|
||||
if: ${{ !matrix.build_only }}
|
||||
timeout-minutes: 15
|
||||
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 -- sccache --show-stats
|
||||
pixi run -- sccache --stop-server || true
|
||||
pixi run -e "$ENV" -- sccache --show-stats
|
||||
pixi run -e "$ENV" -- sccache --stop-server || true
|
||||
else
|
||||
pixi run -- ccache --show-stats
|
||||
pixi run -e "$ENV" -- 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 }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -72,4 +72,3 @@ tests/unit/Local/
|
||||
.claude/*
|
||||
!.claude/CLAUDE.md
|
||||
!.claude/commands/
|
||||
openspec/
|
||||
|
||||
@@ -127,9 +127,16 @@ 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 $<TARGET_FILE:flatc> --cpp -o "${PROJECT_BINARY_DIR}/generated" "${FBS_SCHEMA_FILE}"
|
||||
COMMAND ${FLATC_CMD} --cpp -o "${PROJECT_BINARY_DIR}/generated" "${FBS_SCHEMA_FILE}"
|
||||
DEPENDS "${FBS_SCHEMA_FILE}"
|
||||
COMMENT "Generating C++ header from ${FBS_SCHEMA_FILE}"
|
||||
)
|
||||
@@ -151,13 +158,13 @@ target_link_libraries(clice-core PUBLIC
|
||||
spdlog::spdlog
|
||||
roaring::roaring
|
||||
flatbuffers
|
||||
eventide::ipc::lsp
|
||||
eventide::serde::toml
|
||||
kota::ipc::lsp
|
||||
kota::codec::toml
|
||||
simdjson::simdjson
|
||||
)
|
||||
|
||||
add_executable(clice "${PROJECT_SOURCE_DIR}/src/clice.cc")
|
||||
target_link_libraries(clice PRIVATE clice::core eventide::deco)
|
||||
target_link_libraries(clice PRIVATE clice::core kota::deco)
|
||||
install(TARGETS clice RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
|
||||
add_custom_target(copy_clang_resource ALL
|
||||
@@ -189,7 +196,7 @@ if(CLICE_ENABLE_TEST)
|
||||
"${PROJECT_SOURCE_DIR}/src"
|
||||
"${PROJECT_SOURCE_DIR}/tests/unit"
|
||||
)
|
||||
target_link_libraries(unit_tests PRIVATE clice::core eventide::zest eventide::deco)
|
||||
target_link_libraries(unit_tests PRIVATE clice::core kota::zest kota::deco)
|
||||
endif()
|
||||
|
||||
if(CLICE_ENABLE_BENCHMARK)
|
||||
@@ -199,7 +206,7 @@ if(CLICE_ENABLE_BENCHMARK)
|
||||
target_include_directories(scan_benchmark PRIVATE
|
||||
"${PROJECT_SOURCE_DIR}/src"
|
||||
)
|
||||
target_link_libraries(scan_benchmark PRIVATE clice::core eventide::deco)
|
||||
target_link_libraries(scan_benchmark PRIVATE clice::core kota::deco)
|
||||
endif()
|
||||
|
||||
if(CLICE_RELEASE)
|
||||
|
||||
@@ -21,17 +21,15 @@
|
||||
#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 {
|
||||
@@ -97,7 +95,7 @@ void export_graph_json(const PathPool& path_pool,
|
||||
export_data.files.push_back(std::move(node));
|
||||
}
|
||||
|
||||
auto json = et::serde::json::to_json(export_data);
|
||||
auto json = kota::codec::json::to_json(export_data);
|
||||
if(!json) {
|
||||
std::println(stderr, "Failed to serialize dependency graph");
|
||||
return;
|
||||
@@ -221,8 +219,8 @@ void print_report(const ScanReport& report) {
|
||||
}
|
||||
|
||||
int main(int argc, const char** argv) {
|
||||
auto args = deco::util::argvify(argc, argv);
|
||||
auto result = deco::cli::parse<BenchmarkOptions>(args);
|
||||
auto args = kota::deco::util::argvify(argc, argv);
|
||||
auto result = kota::deco::cli::parse<BenchmarkOptions>(args);
|
||||
|
||||
if(!result.has_value()) {
|
||||
std::println(stderr, "Error: {}", result.error().message);
|
||||
@@ -233,7 +231,7 @@ int main(int argc, const char** argv) {
|
||||
|
||||
if(opts.help.value_or(false) || !opts.cdb_path.has_value()) {
|
||||
std::ostringstream oss;
|
||||
deco::cli::write_usage_for<BenchmarkOptions>(oss, "scan_benchmark [OPTIONS] <cdb>");
|
||||
kota::deco::cli::write_usage_for<BenchmarkOptions>(oss, "scan_benchmark [OPTIONS] <cdb>");
|
||||
std::print("{}", oss.str());
|
||||
return opts.help.value_or(false) ? 0 : 1;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,22 @@ 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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
include_guard()
|
||||
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/llvm.cmake)
|
||||
setup_llvm("21.1.4+r1")
|
||||
setup_llvm("21.1.8")
|
||||
|
||||
# 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(
|
||||
eventide
|
||||
GIT_REPOSITORY https://github.com/clice-io/eventide
|
||||
kotatsu
|
||||
GIT_REPOSITORY https://github.com/clice-io/kotatsu
|
||||
GIT_TAG main
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
FetchContent_MakeAvailable(eventide spdlog croaring flatbuffers)
|
||||
FetchContent_MakeAvailable(kotatsu spdlog croaring flatbuffers)
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
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 "")
|
||||
|
||||
|
||||
@@ -1,83 +1,142 @@
|
||||
[
|
||||
{
|
||||
"version": "21.1.4+r1",
|
||||
"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",
|
||||
"filename": "arm64-macos-clang-debug-asan.tar.xz",
|
||||
"sha256": "7da4b7d63edefecaf11773e7e701c575140d1a07329bbbb038673b6ee4516ff5",
|
||||
"sha256": "b02d20e4f7294ee33f49a09dfdd765b3b44135e003ef50e3a760aeee39e3f993",
|
||||
"lto": false,
|
||||
"asan": true,
|
||||
"platform": "macosx",
|
||||
"arch": "arm64",
|
||||
"build_type": "Debug"
|
||||
},
|
||||
{
|
||||
"version": "21.1.4+r1",
|
||||
"version": "21.1.8",
|
||||
"filename": "arm64-macos-clang-releasedbg-lto.tar.xz",
|
||||
"sha256": "300455b169448f9f01ae95e3bc269f489558a4ca3955e3032171cc75feca0e30",
|
||||
"sha256": "e40c21eb0d0b91d9d4ab31212a5cb01ea46707f5c29839414567857e4147604d",
|
||||
"lto": true,
|
||||
"asan": false,
|
||||
"platform": "macosx",
|
||||
"arch": "arm64",
|
||||
"build_type": "RelWithDebInfo"
|
||||
},
|
||||
{
|
||||
"version": "21.1.4+r1",
|
||||
"version": "21.1.8",
|
||||
"filename": "arm64-macos-clang-releasedbg.tar.xz",
|
||||
"sha256": "9abfc6cd65b957d734ffb97610a634fb4a66d3fbe0fcfb5a1c9124ef693c1495",
|
||||
"sha256": "e1b01de34f0edfd41c118e4981a93afb35556ae369597e864f4a393db623b926",
|
||||
"lto": false,
|
||||
"asan": false,
|
||||
"platform": "macosx",
|
||||
"arch": "arm64",
|
||||
"build_type": "RelWithDebInfo"
|
||||
},
|
||||
{
|
||||
"version": "21.1.4+r1",
|
||||
"version": "21.1.8",
|
||||
"filename": "x64-linux-gnu-debug-asan.tar.xz",
|
||||
"sha256": "c1ad3ec476911596a842ac67dd9c9c9475ce9f0a77b81101d3c801840292e7bc",
|
||||
"sha256": "76bb82d822b5377fb5e0fac8abcfba125142e6a0acc02bb36d1fa1532a268646",
|
||||
"lto": false,
|
||||
"asan": true,
|
||||
"platform": "linux",
|
||||
"arch": "x64",
|
||||
"build_type": "Debug"
|
||||
},
|
||||
{
|
||||
"version": "21.1.4+r1",
|
||||
"version": "21.1.8",
|
||||
"filename": "x64-linux-gnu-releasedbg-lto.tar.xz",
|
||||
"sha256": "8a869c2184d139dbba704e2d712e7a68336458ad2d70622b3eb906c3e3511e54",
|
||||
"sha256": "32f5edddec1e689124f045b586fb402ae30febc05203af7391b088bc8494cd53",
|
||||
"lto": true,
|
||||
"asan": false,
|
||||
"platform": "linux",
|
||||
"arch": "x64",
|
||||
"build_type": "RelWithDebInfo"
|
||||
},
|
||||
{
|
||||
"version": "21.1.4+r1",
|
||||
"version": "21.1.8",
|
||||
"filename": "x64-linux-gnu-releasedbg.tar.xz",
|
||||
"sha256": "552bab86f715d4f2c027f07eaaf5b3d6b8e430af0b74b470142f3f00da4feec6",
|
||||
"sha256": "8ba3c84f23a2a81a86c54780754a61adf99048aa2ac0dc9b9708d0f842d553de",
|
||||
"lto": false,
|
||||
"asan": false,
|
||||
"platform": "linux",
|
||||
"arch": "x64",
|
||||
"build_type": "RelWithDebInfo"
|
||||
},
|
||||
{
|
||||
"version": "21.1.4+r1",
|
||||
"filename": "x64-windows-msvc-debug-asan.tar.xz",
|
||||
"sha256": "093667a493d336c22ff3c604c5f1fea2a7d2c927c1179cec44e9a03726906ac1",
|
||||
"lto": false,
|
||||
"asan": true,
|
||||
"platform": "windows",
|
||||
"build_type": "Debug"
|
||||
"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.4+r1",
|
||||
"version": "21.1.8",
|
||||
"filename": "x64-macos-clang-releasedbg.tar.xz",
|
||||
"sha256": "53c13f8e1082fa2fe2f9c05303de48cb3133bf5f24271f4b3062f1dec578159c",
|
||||
"lto": false,
|
||||
"asan": false,
|
||||
"platform": "macosx",
|
||||
"arch": "x64",
|
||||
"build_type": "RelWithDebInfo"
|
||||
},
|
||||
{
|
||||
"version": "21.1.8",
|
||||
"filename": "x64-windows-msvc-releasedbg-lto.tar.xz",
|
||||
"sha256": "010539e85621dc3c6ecf359d899feb4075aeca5d0bba6625cdbec0e570e79129",
|
||||
"sha256": "16bcf0e4cbc3d2b1204edd619a3837004dacea28eeff0a101c8d0212f936427d",
|
||||
"lto": true,
|
||||
"asan": false,
|
||||
"platform": "windows",
|
||||
"arch": "x64",
|
||||
"build_type": "RelWithDebInfo"
|
||||
},
|
||||
{
|
||||
"version": "21.1.4+r1",
|
||||
"version": "21.1.8",
|
||||
"filename": "x64-windows-msvc-releasedbg.tar.xz",
|
||||
"sha256": "f473c09fbea10053fac00be409d75dc228d4a38bcbc5e4aeb58b56a4b0dde78e",
|
||||
"sha256": "81d31fad05e200726c8178314b0b2045c947483dddd8cb974f4c376ae5f441fa",
|
||||
"lto": false,
|
||||
"asan": false,
|
||||
"platform": "windows",
|
||||
"arch": "x64",
|
||||
"build_type": "RelWithDebInfo"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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 `eventide::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 `kota::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 `et::mutex` (strand) to serialize compilation and feature queries. Heavy work (compilation, feature extraction) runs on a thread pool via `et::queue`.
|
||||
- **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`.
|
||||
|
||||
## 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 `et::queue`.
|
||||
All requests are dispatched to a thread pool via `kota::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 `et::cancellation_source` to abort in-flight compilations when files are invalidated
|
||||
- **Cancellation**: Uses `kota::cancellation_source` to abort in-flight compilations when files are invalidated
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-22
|
||||
185
openspec/changes/split-folding-range-pipeline/design.md
Normal file
185
openspec/changes/split-folding-range-pipeline/design.md
Normal file
@@ -0,0 +1,185 @@
|
||||
## Context
|
||||
|
||||
This change extracts decision `2` from `openspec/changes/explore-improve-folding-range-support/design.md` into a standalone proposal. The current folding implementation in `src/feature/folding_ranges.cpp` mixes three responsibilities in one path:
|
||||
|
||||
- discovering foldable structure from AST data
|
||||
- deciding which ranges survive deduplication and validation
|
||||
- shaping the final LSP response, including output metadata
|
||||
|
||||
That coupling makes the code harder to extend safely. Comment folding, directive-based collectors, capability-aware rendering, and range limiting all become riskier when collection and rendering rules share the same code path. The extracted proposal keeps scope narrower: it does not add new fold categories by itself, but it creates the architecture that later changes can build on without destabilizing existing structural folding.
|
||||
|
||||
The downloaded clangd reference confirms both the value and the limit of the upstream design. clangd has useful, tested folding behavior for brace bodies, comment blocks, contiguous `//` groups, and `lineFoldingOnly`, but its implementation largely emits protocol-shaped `FoldingRange` objects directly from collection code. In `SemanticSelection.cpp`, both the AST path and the pseudo-parser path build `FoldingRange` results directly, and the pseudo-parser applies rendering details such as delimiter trimming and `lineFoldingOnly` adjustments while collecting ranges. That is a good behavior reference, but it is not the architecture this extracted change should copy.
|
||||
|
||||
`clice` already has stronger ingredients for a real pipeline:
|
||||
|
||||
- `LocalSourceRange` gives us a main-file, half-open offset representation that is independent of LSP position encoding
|
||||
- directive metadata already captures information clangd does not expose well, including conditional-branch state, pragma regions, includes, imports, and macro references
|
||||
- the current tests are boundary-oriented, which makes them a good fit for validating raw spans before protocol rendering
|
||||
|
||||
The design therefore separates "what fold exists in the source" from "how that fold should be emitted to this client". clangd's tested boundary rules are still relevant, but they should become renderer policy and normalization rules rather than collector output format.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Separate folding processing into collection, normalization, and rendering phases.
|
||||
- Preserve the existing AST structural folding categories already supported by `clice`.
|
||||
- Make ordering, deduplication, and boundary validation deterministic and testable.
|
||||
- Give later changes a stable extension point for comments, directives, and client-driven rendering options.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Add comment folding in this change.
|
||||
- Fix preprocessor branch-closing behavior in this change.
|
||||
- Add new fold categories such as macro definitions or include/import grouping.
|
||||
- Depend on initialize-time client capability plumbing being implemented first.
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Use clangd as a behavior reference, not an architecture template
|
||||
|
||||
This change should borrow clangd's confirmed folding behavior where it is useful, especially around multiline comments, contiguous `//` comment groups, main-file-only filtering, and `lineFoldingOnly` boundary shaping. It should not copy clangd's habit of emitting protocol-shaped `FoldingRange` objects directly from collection logic.
|
||||
|
||||
Why:
|
||||
|
||||
- clangd's tests are valuable because they pin down tricky folding behavior around comments, macro boundaries, and line-only rendering
|
||||
- clangd's data flow is intentionally narrow and mixes collection with response shaping
|
||||
- `clice` already has richer file-local and directive metadata that supports a cleaner internal representation
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- Treat clangd's direct `FoldingRange` construction as the architecture to reproduce. Rejected because it would preserve the same coupling this extracted change is meant to remove.
|
||||
|
||||
### 2. Introduce a raw internal folding-range model
|
||||
|
||||
Collectors should emit an internal `RawFoldingRange`-style structure instead of final LSP protocol objects. The raw model should preserve source locations, an internal category, and optional metadata hints that later phases may use.
|
||||
|
||||
The raw model should be shaped around file-local source structure, not LSP transport fields. At minimum it should carry:
|
||||
|
||||
- a main-file `LocalSourceRange` span using half-open byte offsets
|
||||
- an internal fold category such as namespace, record, access section, function body, comment block, comment group, conditional branch, pragma region, include group, or import group
|
||||
- the collector origin, such as AST, comment scanning, or directive metadata, so normalization has a stable tie-break and debugging surface
|
||||
- render hints for syntax-specific shaping, such as delimiter trimming, whether line-only folding should hide the final line, and an optional collapsed-text hint
|
||||
|
||||
In other words, the raw model should look closer to:
|
||||
|
||||
```cpp
|
||||
struct RawFoldRenderHint {
|
||||
std::uint8_t trim_start_bytes = 0;
|
||||
std::uint8_t trim_end_bytes = 0;
|
||||
bool hide_last_line_when_line_only = false;
|
||||
std::string collapsed_text_hint;
|
||||
};
|
||||
|
||||
struct RawFoldingRange {
|
||||
LocalSourceRange span;
|
||||
RawFoldCategory category;
|
||||
RawFoldOrigin origin;
|
||||
RawFoldRenderHint render;
|
||||
};
|
||||
```
|
||||
|
||||
The important design choice is that `span` represents the foldable source envelope in the main file, while renderer-specific trimming stays in `render` hints. For example:
|
||||
|
||||
- brace-based structural folds keep the full braced span and let the renderer trim interior boundaries
|
||||
- block comments keep the full `/* ... */` span and let the renderer decide whether to hide the closing delimiter or final line
|
||||
- contiguous `//` groups keep the full grouped span and let the renderer decide how much of the opening sentinel remains visible
|
||||
|
||||
Why:
|
||||
|
||||
- collectors should describe what was found, not how it will be serialized
|
||||
- `LocalSourceRange` is already the natural coordinate system for `clice`
|
||||
- public LSP kinds such as `comment`, `imports`, and `region` are too lossy to use as the internal category model
|
||||
- future comment and directive collectors can share the same pipeline contract
|
||||
- tests can validate collection independently from rendering
|
||||
|
||||
Alternatives considered:
|
||||
|
||||
- Continue emitting LSP ranges directly from collectors. Rejected because it keeps protocol concerns entangled with source discovery.
|
||||
- Make the raw model store already-trimmed visible interior spans instead of the full source envelope. Rejected because line-only rendering, collapsed text, and comment delimiter rules would still leak back into every collector.
|
||||
|
||||
### 3. Normalize ranges before rendering
|
||||
|
||||
All collected ranges should pass through a normalization step before any response is emitted. Normalization is responsible for deterministic ordering, duplicate removal, and rejection of degenerate or unmappable ranges.
|
||||
|
||||
Normalization should operate on raw spans and internal categories, not on final LSP fields. Its responsibilities include:
|
||||
|
||||
- deterministic ordering independent of collector traversal order
|
||||
- duplicate collapse for collectors that discover the same fold
|
||||
- invalid-range filtering after raw spans and render hints are reconciled
|
||||
- stable tie-breaking for overlapping ranges from different origins
|
||||
|
||||
Collectors may still reject obviously invalid inputs, such as non-main-file locations that cannot be mapped to `LocalSourceRange`, but normalization remains the phase that decides which collected folds survive to rendering.
|
||||
|
||||
Why:
|
||||
|
||||
- duplicate or invalid ranges are easier to reason about in one place than across many collectors
|
||||
- stable ordering reduces regression noise and makes range limiting predictable later
|
||||
- category-aware normalization preserves internal meaning until the renderer maps it to public kinds
|
||||
- normalization lets new collectors plug in without each collector re-implementing cleanup logic
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- Let each collector manage its own sorting and duplicate suppression. Rejected because cross-collector interactions would still remain undefined.
|
||||
|
||||
### 4. Keep the current AST visitor as the first collector boundary
|
||||
|
||||
The initial extraction should preserve the current AST visitor as one collector feeding the raw model. This reduces refactor risk while still creating the new phase boundaries.
|
||||
|
||||
Why:
|
||||
|
||||
- the existing structural fold coverage is valuable and should not be rewritten unnecessarily
|
||||
- an adapter-style refactor is easier to verify against current tests than a full collector redesign
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- Rewrite collection around a brand-new multi-source manager immediately. Rejected because it adds scope before the phase split is proven.
|
||||
|
||||
### 5. Move output shaping into a dedicated renderer
|
||||
|
||||
The renderer should translate normalized ranges into LSP folding ranges. Boundary shaping, output kinds, and optional metadata emission should live there, even if some options still use default values until later protocol plumbing exists.
|
||||
|
||||
Renderer input should be the normalized raw model plus a separate `FoldingRenderOptions` structure. The renderer then becomes responsible for:
|
||||
|
||||
- converting `LocalSourceRange` into protocol positions for the requested encoding
|
||||
- applying delimiter trimming and line-only adjustments
|
||||
- mapping internal categories to public LSP kinds
|
||||
- deciding whether collapsed text is emitted or suppressed
|
||||
- later applying deterministic `rangeLimit` trimming without changing collectors
|
||||
|
||||
This is the key point where `clice` should intentionally diverge from clangd. clangd threads `lineFoldingOnly` into collection and directly produces protocol objects. `clice` should keep those capability and transport decisions isolated in rendering so collectors remain stable as client support evolves.
|
||||
|
||||
Why:
|
||||
|
||||
- rendering rules are a separate concern from source discovery
|
||||
- later work on line-only output, metadata gating, or public kind mapping should not force collector rewrites
|
||||
- clangd-style line-only shaping is still supported, but as renderer policy rather than collector output
|
||||
- isolating rendering makes behavioral diffs easier to review
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- Keep final boundary shaping next to the AST collector and only add a small helper for sorting. Rejected because it only moves a symptom, not the architectural problem.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Refactoring the current path can accidentally change fold ordering] -> Mitigation: add deterministic-order assertions and compare outputs for existing structural fixtures.
|
||||
- [The raw model could become too abstract too early] -> Mitigation: keep the initial fields minimal and only include data already needed by current structural folds.
|
||||
- [Full-envelope raw spans plus render hints may feel less direct than storing already-trimmed ranges] -> Mitigation: use a small, explicit render-hint structure and validate brace/comment shaping with focused renderer tests.
|
||||
- [A renderer abstraction may appear premature before full capability plumbing exists] -> Mitigation: keep default render options aligned with current behavior and treat future options as extension points, not immediate scope.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Introduce raw folding-range and render-option types behind the existing entrypoint.
|
||||
2. Convert the current AST-based collectors to emit raw ranges.
|
||||
3. Insert normalization between collection and response emission.
|
||||
4. Move LSP object construction into a dedicated renderer.
|
||||
5. Verify that existing structural folding fixtures still produce the expected ranges.
|
||||
|
||||
Rollback strategy:
|
||||
|
||||
- If the refactor destabilizes output, keep the new helper types but temporarily route the old direct-emission path until normalization and rendering regressions are resolved.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Whether public kind remapping should land in this extracted change or remain a follow-up proposal once the renderer boundary exists.
|
||||
- Whether collector origin should remain part of the long-term raw model after normalization policy stabilizes, or only exist temporarily as a debugging and tie-break aid.
|
||||
26
openspec/changes/split-folding-range-pipeline/proposal.md
Normal file
26
openspec/changes/split-folding-range-pipeline/proposal.md
Normal file
@@ -0,0 +1,26 @@
|
||||
## Why
|
||||
|
||||
`explore-improve-folding-range-support` combines several different concerns: upstream comparison work, baseline folding fixes, preprocessor extensions, and an internal refactor. The second design point in that change, splitting the folding-range pipeline into collection, normalization, and rendering, is the architectural slice that other work depends on and should be referenceable as its own proposal.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Extract the pipeline-splitting work from `explore-improve-folding-range-support` into a standalone change focused on folding-range architecture.
|
||||
- Introduce an internal raw folding-range model so collectors no longer emit final LSP objects directly.
|
||||
- Define a normalization phase that performs deterministic sorting, duplicate removal, and boundary validation before response generation.
|
||||
- Define a rendering phase that owns line/column shaping and optional metadata emission instead of mixing those concerns into collectors.
|
||||
- Preserve the current AST structural folding coverage while establishing extension points for future comment, directive, and capability-aware rendering work.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `folding-range-pipeline`: Provide a deterministic folding-range pipeline that separates collection, normalization, and rendering while preserving existing structural folds.
|
||||
|
||||
### Modified Capabilities
|
||||
- None.
|
||||
|
||||
## Impact
|
||||
|
||||
- `src/feature/folding_ranges.cpp` will be refactored around raw-range collection, normalization, and rendering boundaries.
|
||||
- Folding-related helper types may be introduced near the folding feature implementation.
|
||||
- `tests/unit/feature/folding_range_tests.cpp` will need regression coverage for structural folds and deterministic ordering.
|
||||
- `openspec/changes/explore-improve-folding-range-support/design.md` remains the source change from which this standalone proposal was extracted.
|
||||
@@ -0,0 +1,38 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Folding ranges are normalized before response emission
|
||||
The server SHALL convert collected folding candidates into a deterministic normalized set before emitting the folding range response.
|
||||
|
||||
#### Scenario: Duplicate candidates collapse to one emitted fold
|
||||
- **WHEN** multiple collectors produce the same folding candidate for the same source span and internal category
|
||||
- **THEN** the server MUST emit at most one folding range for that candidate
|
||||
|
||||
#### Scenario: Invalid candidates are dropped during normalization
|
||||
- **WHEN** a collected folding candidate does not span multiple lines or cannot be mapped back to the main file
|
||||
- **THEN** the server MUST omit that candidate from the emitted folding ranges
|
||||
|
||||
#### Scenario: Output ordering is deterministic
|
||||
- **WHEN** the same document is analyzed repeatedly without source changes
|
||||
- **THEN** the server MUST emit folding ranges in a deterministic order that does not depend on collector traversal order
|
||||
|
||||
### Requirement: Existing structural folding survives the pipeline split
|
||||
The server SHALL preserve the currently supported AST structural folding categories after collection, normalization, and rendering are separated.
|
||||
|
||||
#### Scenario: Supported structural regions remain foldable
|
||||
- **WHEN** a document contains a supported multi-line namespace, record, function body, parameter list, lambda body, initializer list, call argument list, or compound statement
|
||||
- **THEN** the server MUST still return a folding range for that region when its boundaries can be mapped to the main file
|
||||
|
||||
#### Scenario: Structural coverage is preserved through normalization
|
||||
- **WHEN** the document contains only currently supported AST-driven folding categories
|
||||
- **THEN** normalization and rendering MUST NOT remove a valid structural fold except when it is an exact duplicate or an invalid range
|
||||
|
||||
### Requirement: Rendering decisions are applied after normalization
|
||||
The server SHALL derive final LSP folding-range output from normalized internal ranges instead of requiring collectors to emit protocol-shaped results directly.
|
||||
|
||||
#### Scenario: Rendering options do not require collector changes
|
||||
- **WHEN** rendering rules change how line or metadata output is shaped for a normalized fold
|
||||
- **THEN** the server MUST apply that change in the rendering phase without requiring collector-specific logic changes
|
||||
|
||||
#### Scenario: Metadata hints remain optional until rendering
|
||||
- **WHEN** a collected or normalized fold carries optional kind or collapsed-text hints
|
||||
- **THEN** the renderer MUST decide whether to surface, transform, or suppress that metadata in the emitted LSP range
|
||||
16
openspec/changes/split-folding-range-pipeline/tasks.md
Normal file
16
openspec/changes/split-folding-range-pipeline/tasks.md
Normal file
@@ -0,0 +1,16 @@
|
||||
## 1. Raw Model and Collector Boundary
|
||||
|
||||
- [ ] 1.1 Introduce internal raw folding-range and render-option types while keeping the current folding entrypoint stable.
|
||||
- [ ] 1.2 Convert the existing AST structural folding path in `src/feature/folding_ranges.cpp` to emit raw ranges instead of final LSP ranges.
|
||||
- [ ] 1.3 Add regression fixtures or assertions that cover the currently supported structural fold categories before further refactoring.
|
||||
|
||||
## 2. Normalization and Rendering
|
||||
|
||||
- [ ] 2.1 Implement normalization for deterministic sorting, duplicate removal, and invalid-range filtering.
|
||||
- [ ] 2.2 Introduce a dedicated renderer that converts normalized ranges into final LSP folding-range objects.
|
||||
- [ ] 2.3 Keep default rendered output compatible with current structural behavior while exposing extension points for future collectors and render rules.
|
||||
|
||||
## 3. Verification
|
||||
|
||||
- [ ] 3.1 Compare pre-refactor and post-refactor outputs for the existing structural folding test cases.
|
||||
- [ ] 3.2 Run relevant folding-range unit tests and fix any ordering, deduplication, or boundary regressions introduced by the new pipeline.
|
||||
86
pixi.toml
86
pixi.toml
@@ -14,17 +14,24 @@ 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"]
|
||||
platforms = ["win-64", "linux-64", "osx-arm64", "osx-64", "linux-aarch64", "win-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"
|
||||
@@ -35,6 +42,7 @@ 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 = "*"
|
||||
@@ -54,24 +62,69 @@ 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]
|
||||
[feature.package.tasks.package-config]
|
||||
args = [{ arg = "type", default = "RelWithDebInfo" }]
|
||||
cmd = """
|
||||
cmake -B build/RelWithDebInfo -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
cmake -B build/{{ type }} -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE={{ type }} \
|
||||
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchain.cmake \
|
||||
-DCLICE_RELEASE=ON && \
|
||||
cmake --build build/RelWithDebInfo
|
||||
-DCLICE_RELEASE=ON
|
||||
"""
|
||||
|
||||
[feature.package.tasks.package]
|
||||
args = [{ arg = "type", default = "RelWithDebInfo" }]
|
||||
depends-on = [
|
||||
{ task = "package-config", args = ["{{ type }}"] },
|
||||
{ task = "cmake-build", args = ["{{ type }}"] },
|
||||
]
|
||||
|
||||
# ============================================================================== #
|
||||
# CMAKE #
|
||||
# ============================================================================== #
|
||||
@@ -79,14 +132,13 @@ cmake --build build/RelWithDebInfo
|
||||
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 }} {{extra}} \
|
||||
-DCLICE_CI_ENVIRONMENT={{ ci }}
|
||||
"""
|
||||
|
||||
[feature.build.tasks.cmake-build]
|
||||
@@ -97,10 +149,9 @@ 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 }}", "{{extra}}"] },
|
||||
{ task = "cmake-config", args = ["{{ type }}", "{{ ci }}"] },
|
||||
{ task = "cmake-build", args = ["{{ type }}"] },
|
||||
]
|
||||
|
||||
@@ -108,15 +159,15 @@ depends-on = [
|
||||
args = [{ arg = "type", default = "RelWithDebInfo" }]
|
||||
depends-on = [{ task = "lint-cpp", args = ["{{ type }}"] }]
|
||||
|
||||
[feature.build.tasks.unit-test]
|
||||
[feature.test.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 tests/integration \
|
||||
--executable=./build/{{ type }}/bin/clice
|
||||
pytest -s --log-cli-level=INFO --timeout=300 --timeout-method=thread \
|
||||
tests/integration --executable=./build/{{ type }}/bin/clice
|
||||
"""
|
||||
|
||||
[feature.test.tasks.smoke-test]
|
||||
@@ -131,6 +182,7 @@ args = [{ arg = "type", default = "RelWithDebInfo" }]
|
||||
depends-on = [
|
||||
{ task = "unit-test", args = ["{{ type }}"] },
|
||||
{ task = "integration-test", args = ["{{ type }}"] },
|
||||
{ task = "smoke-test", args = ["{{ type }}"] },
|
||||
]
|
||||
|
||||
# ============================================================================== #
|
||||
@@ -152,9 +204,14 @@ 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 = "*"
|
||||
@@ -180,6 +237,9 @@ 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 = "*"
|
||||
|
||||
12
scripts/activate_cross_linux.sh
Normal file
12
scripts/activate_cross_linux.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/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=
|
||||
8
scripts/activate_cross_macos.sh
Normal file
8
scripts/activate_cross_macos.sh
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/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=
|
||||
8
scripts/activate_cross_windows.bat
Normal file
8
scripts/activate_cross_windows.bat
Normal file
@@ -0,0 +1,8 @@
|
||||
@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="
|
||||
@@ -4,6 +4,7 @@ import subprocess
|
||||
import shutil
|
||||
import argparse
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -22,6 +23,66 @@ 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."
|
||||
@@ -48,6 +109,10 @@ 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()
|
||||
|
||||
@@ -85,118 +150,46 @@ 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("---------------------")
|
||||
|
||||
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_path = Path(__file__).resolve().parent / "llvm-components.json"
|
||||
with components_path.open() as f:
|
||||
llvm_distribution_components = json.load(f)["components"]
|
||||
|
||||
components_joined = ";".join(llvm_distribution_components)
|
||||
cmake_args = [
|
||||
"-G",
|
||||
"Ninja",
|
||||
f"-DCMAKE_TOOLCHAIN_FILE={toolchain_file.as_posix()}",
|
||||
f"-DCMAKE_INSTALL_PREFIX={install_prefix}",
|
||||
"-DCMAKE_C_FLAGS=-w",
|
||||
"-DCMAKE_CXX_FLAGS=-w",
|
||||
]
|
||||
|
||||
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 += [
|
||||
"-DLLVM_ENABLE_ZLIB=OFF",
|
||||
"-DLLVM_ENABLE_ZSTD=OFF",
|
||||
"-DLLVM_ENABLE_LIBXML2=OFF",
|
||||
@@ -231,7 +224,6 @@ 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}",
|
||||
@@ -256,8 +248,10 @@ def main():
|
||||
is_shared = "OFF"
|
||||
if args.mode == "Debug":
|
||||
cmake_args.append("-DCMAKE_BUILD_TYPE=Debug")
|
||||
cmake_args.append("-DLLVM_USE_SANITIZER=Address")
|
||||
is_shared = "ON"
|
||||
# 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"
|
||||
elif args.mode == "Release":
|
||||
cmake_args.append("-DCMAKE_BUILD_TYPE=Release")
|
||||
elif args.mode == "RelWithDebInfo":
|
||||
@@ -272,6 +266,24 @@ 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}...")
|
||||
|
||||
99
scripts/llvm-components.json
Normal file
99
scripts/llvm-components.json
Normal file
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -40,23 +40,52 @@ 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
|
||||
manifest: list[dict],
|
||||
version: str,
|
||||
build_type: str,
|
||||
is_lto: bool,
|
||||
platform: str,
|
||||
arch: 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"build_type={build_type}, lto={is_lto}"
|
||||
f"arch={arch}, build_type={build_type}, lto={is_lto}"
|
||||
)
|
||||
|
||||
|
||||
@@ -264,6 +293,14 @@ 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()
|
||||
|
||||
@@ -275,8 +312,11 @@ def main() -> None:
|
||||
)
|
||||
token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
|
||||
build_type = args.build_type
|
||||
platform_name = detect_platform()
|
||||
log(f"Platform detected: {platform_name}, normalized build type: {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}"
|
||||
)
|
||||
manifest = read_manifest(Path(args.manifest))
|
||||
|
||||
binary_dir = Path(args.binary_dir).resolve()
|
||||
@@ -304,7 +344,12 @@ def main() -> None:
|
||||
if install_path is None:
|
||||
needs_install = True
|
||||
artifact = pick_artifact(
|
||||
manifest, args.version, build_type, args.enable_lto, platform_name
|
||||
manifest,
|
||||
args.version,
|
||||
build_type,
|
||||
args.enable_lto,
|
||||
platform_name,
|
||||
arch_name,
|
||||
)
|
||||
log(f"Selected artifact: {artifact.get('filename')} for download")
|
||||
filename = artifact["filename"]
|
||||
@@ -317,7 +362,12 @@ def main() -> None:
|
||||
install_path = install_root
|
||||
elif needs_install:
|
||||
artifact = pick_artifact(
|
||||
manifest, args.version, build_type, args.enable_lto, platform_name
|
||||
manifest,
|
||||
args.version,
|
||||
build_type,
|
||||
args.enable_lto,
|
||||
platform_name,
|
||||
arch_name,
|
||||
)
|
||||
log(f"Selected artifact: {artifact.get('filename')} for download")
|
||||
filename = artifact["filename"]
|
||||
|
||||
162
scripts/update-llvm-version.py
Executable file
162
scripts/update-llvm-version.py
Executable file
@@ -0,0 +1,162 @@
|
||||
#!/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()
|
||||
@@ -27,6 +27,15 @@ 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:
|
||||
@@ -43,6 +52,7 @@ 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),
|
||||
}
|
||||
|
||||
|
||||
163
scripts/validate-llvm-components.py
Executable file
163
scripts/validate-llvm-components.py
Executable file
@@ -0,0 +1,163 @@
|
||||
#!/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()
|
||||
50
src/clice.cc
50
src/clice.cc
@@ -4,19 +4,21 @@
|
||||
#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 deco::decl::KVStyle;
|
||||
using kota::deco::decl::KVStyle;
|
||||
|
||||
struct Options {
|
||||
DecoKV(style = KVStyle::JoinedOrSeparate,
|
||||
@@ -72,8 +74,8 @@ int main(int argc, const char** argv) {
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
#endif
|
||||
|
||||
auto args = deco::util::argvify(argc, argv);
|
||||
auto result = deco::cli::parse<clice::Options>(args);
|
||||
auto args = kota::deco::util::argvify(argc, argv);
|
||||
auto result = kota::deco::cli::parse<clice::Options>(args);
|
||||
|
||||
if(!result.has_value()) {
|
||||
LOG_ERROR("{}", result.error().message);
|
||||
@@ -83,7 +85,7 @@ int main(int argc, const char** argv) {
|
||||
auto& opts = result->options;
|
||||
|
||||
if(opts.help.value_or(false)) {
|
||||
deco::cli::write_usage_for<clice::Options>(std::cout, "clice [OPTIONS]");
|
||||
kota::deco::cli::write_usage_for<clice::Options>(std::cout, "clice [OPTIONS]");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -132,23 +134,22 @@ int main(int argc, const char** argv) {
|
||||
if(mode == "pipe") {
|
||||
clice::logging::stderr_logger("master", clice::logging::options);
|
||||
|
||||
namespace et = eventide;
|
||||
et::event_loop loop;
|
||||
kota::event_loop loop;
|
||||
|
||||
auto transport = et::ipc::StreamTransport::open_stdio(loop);
|
||||
auto transport = kota::ipc::StreamTransport::open_stdio(loop);
|
||||
if(!transport) {
|
||||
LOG_ERROR("failed to open stdio transport");
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::unique_ptr<et::ipc::Transport> final_transport = std::move(*transport);
|
||||
std::unique_ptr<kota::ipc::Transport> final_transport = std::move(*transport);
|
||||
if(opts.record.has_value()) {
|
||||
final_transport =
|
||||
std::make_unique<et::ipc::RecordingTransport>(std::move(final_transport),
|
||||
*opts.record);
|
||||
std::make_unique<kota::ipc::RecordingTransport>(std::move(final_transport),
|
||||
*opts.record);
|
||||
}
|
||||
|
||||
et::ipc::JsonPeer peer(loop, std::move(final_transport));
|
||||
kota::ipc::JsonPeer peer(loop, std::move(final_transport));
|
||||
clice::MasterServer server(loop, peer, std::move(self_path));
|
||||
server.register_handlers();
|
||||
|
||||
@@ -160,13 +161,12 @@ int main(int argc, const char** argv) {
|
||||
if(mode == "socket") {
|
||||
clice::logging::stderr_logger("master", clice::logging::options);
|
||||
|
||||
namespace et = eventide;
|
||||
et::event_loop loop;
|
||||
kota::event_loop loop;
|
||||
|
||||
auto host = opts.host.value_or("127.0.0.1");
|
||||
auto port = opts.port.value_or(50051);
|
||||
|
||||
auto acceptor = et::tcp::listen(host, port, {}, loop);
|
||||
auto acceptor = kota::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 = [&]() -> et::task<> {
|
||||
auto task = [&]() -> kota::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<et::ipc::Transport> transport =
|
||||
std::make_unique<et::ipc::StreamTransport>(std::move(client.value()));
|
||||
std::unique_ptr<kota::ipc::Transport> transport =
|
||||
std::make_unique<kota::ipc::StreamTransport>(std::move(client.value()));
|
||||
if(opts.record.has_value()) {
|
||||
transport = std::make_unique<et::ipc::RecordingTransport>(std::move(transport),
|
||||
*opts.record);
|
||||
transport = std::make_unique<kota::ipc::RecordingTransport>(std::move(transport),
|
||||
*opts.record);
|
||||
}
|
||||
et::ipc::JsonPeer peer(loop, std::move(transport));
|
||||
kota::ipc::JsonPeer peer(loop, std::move(transport));
|
||||
clice::MasterServer server(loop, peer, std::string(self_path));
|
||||
server.register_handlers();
|
||||
|
||||
|
||||
@@ -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 {}",
|
||||
eventide::refl::enum_name(family),
|
||||
kota::meta::enum_name(family),
|
||||
driver);
|
||||
|
||||
std::vector<const char*> result;
|
||||
|
||||
@@ -219,9 +219,10 @@ public:
|
||||
|
||||
auto CreateASTConsumer(clang::CompilerInstance& instance, llvm::StringRef file)
|
||||
-> std::unique_ptr<clang::ASTConsumer> final {
|
||||
return std::make_unique<ProxyASTConsumer>(
|
||||
WrapperFrontendAction::CreateASTConsumer(instance, file),
|
||||
unit);
|
||||
auto consumer = WrapperFrontendAction::CreateASTConsumer(instance, file);
|
||||
if(!consumer)
|
||||
return nullptr;
|
||||
return std::make_unique<ProxyASTConsumer>(std::move(consumer), unit);
|
||||
}
|
||||
|
||||
/// Make this public.
|
||||
|
||||
@@ -81,7 +81,8 @@ auto CompilationUnitRef::file_offset(clang::SourceLocation location) -> std::uin
|
||||
}
|
||||
|
||||
auto CompilationUnitRef::file_path(clang::FileID fid) -> llvm::StringRef {
|
||||
assert(fid.isValid() && "Invalid fid");
|
||||
if(!fid.isValid())
|
||||
return {};
|
||||
if(auto it = self->path_cache.find(fid); it != self->path_cache.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ public:
|
||||
const clang::Token& include_tok,
|
||||
llvm::StringRef,
|
||||
bool,
|
||||
clang::CharSourceRange filename_range,
|
||||
clang::CharSourceRange,
|
||||
clang::OptionalFileEntryRef,
|
||||
llvm::StringRef,
|
||||
llvm::StringRef,
|
||||
@@ -108,7 +108,6 @@ public:
|
||||
unit->directives[prev_fid].includes.emplace_back(Include{
|
||||
.fid = {},
|
||||
.location = include_tok.getLocation(),
|
||||
.filename_range = filename_range.getAsRange(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -20,11 +20,8 @@ struct Include {
|
||||
/// The file id of included file.
|
||||
clang::FileID fid;
|
||||
|
||||
/// Location of the `include`.
|
||||
/// Location of the `include` keyword.
|
||||
clang::SourceLocation location;
|
||||
|
||||
/// The range of filename(includes `""` or `<>`).
|
||||
clang::SourceRange filename_range;
|
||||
};
|
||||
|
||||
/// Information about `__has_include` directive.
|
||||
|
||||
@@ -296,7 +296,8 @@ public:
|
||||
llvm::StringRef overload_key,
|
||||
llvm::StringRef signature = {},
|
||||
llvm::StringRef return_type = {},
|
||||
bool is_snippet = false) {
|
||||
bool is_snippet = false,
|
||||
bool is_deprecated = false) {
|
||||
if(label.empty()) {
|
||||
return;
|
||||
}
|
||||
@@ -327,6 +328,9 @@ 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,
|
||||
@@ -355,6 +359,9 @@ public:
|
||||
}
|
||||
item.label_details = std::move(details);
|
||||
}
|
||||
if(is_deprecated) {
|
||||
item.tags = std::vector{protocol::CompletionItemTag::Deprecated};
|
||||
}
|
||||
collected.push_back(std::move(item));
|
||||
};
|
||||
|
||||
@@ -431,13 +438,15 @@ 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);
|
||||
has_snippet,
|
||||
deprecated);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
#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 = eventide::ipc::lsp;
|
||||
namespace lsp = kota::ipc::lsp;
|
||||
|
||||
auto to_uri(llvm::StringRef file) -> std::string {
|
||||
const auto file_view = std::string_view(file.data(), file.size());
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "feature/feature.h"
|
||||
#include "syntax/lexer.h"
|
||||
|
||||
namespace clice::feature {
|
||||
|
||||
namespace {} // namespace
|
||||
|
||||
auto document_links(CompilationUnitRef unit, PositionEncoding encoding)
|
||||
-> std::vector<protocol::DocumentLink> {
|
||||
std::vector<protocol::DocumentLink> links;
|
||||
@@ -22,50 +20,42 @@ 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();
|
||||
|
||||
links.reserve(directives.includes.size() + directives.has_includes.size());
|
||||
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);
|
||||
if(!range)
|
||||
return;
|
||||
protocol::DocumentLink link{.range = to_range(converter, *range)};
|
||||
link.target = target.str();
|
||||
links.push_back(std::move(link));
|
||||
};
|
||||
|
||||
for(const auto& include: directives.includes) {
|
||||
auto [fid, range] = unit.decompose_range(include.filename_range);
|
||||
if(fid != interested || !range.valid()) {
|
||||
continue;
|
||||
if(include.fid.isValid()) {
|
||||
add_link(include.location, unit.file_path(include.fid));
|
||||
}
|
||||
|
||||
protocol::DocumentLink link{
|
||||
.range = to_range(converter, range),
|
||||
};
|
||||
link.target = std::string(unit.file_path(include.fid));
|
||||
links.push_back(std::move(link));
|
||||
}
|
||||
|
||||
for(const auto& has_include: directives.has_includes) {
|
||||
if(has_include.fid.isInvalid()) {
|
||||
continue;
|
||||
if(has_include.fid.isValid()) {
|
||||
add_link(has_include.location, unit.file_path(has_include.fid));
|
||||
}
|
||||
}
|
||||
|
||||
auto [fid, offset] = unit.decompose_location(has_include.location);
|
||||
if(fid != interested || offset >= content.size()) {
|
||||
continue;
|
||||
for(const auto& embed: directives.embeds) {
|
||||
if(embed.file) {
|
||||
add_link(embed.loc, embed.file->getName());
|
||||
}
|
||||
}
|
||||
|
||||
auto tail = content.substr(offset);
|
||||
char open = tail.front();
|
||||
if(open != '<' && open != '"') {
|
||||
continue;
|
||||
for(const auto& has_embed: directives.has_embeds) {
|
||||
if(has_embed.file) {
|
||||
add_link(has_embed.loc, has_embed.file->getName());
|
||||
}
|
||||
|
||||
char close = open == '<' ? '>' : '"';
|
||||
auto close_index = tail.find(close, 1);
|
||||
if(close_index == llvm::StringRef::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
LocalSourceRange range(offset, offset + static_cast<std::uint32_t>(close_index + 1));
|
||||
protocol::DocumentLink link{
|
||||
.range = to_range(converter, range),
|
||||
};
|
||||
link.target = std::string(unit.file_path(has_include.fid));
|
||||
links.push_back(std::move(link));
|
||||
}
|
||||
|
||||
return links;
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
|
||||
#include "compile/compilation.h"
|
||||
#include "compile/compilation_unit.h"
|
||||
#include "eventide/ipc/lsp/position.h"
|
||||
#include "eventide/ipc/lsp/protocol.h"
|
||||
|
||||
#include "kota/ipc/lsp/position.h"
|
||||
#include "kota/ipc/lsp/protocol.h"
|
||||
|
||||
namespace clang {
|
||||
|
||||
@@ -18,11 +19,11 @@ class NamedDecl;
|
||||
|
||||
namespace clice::feature {
|
||||
|
||||
namespace protocol = eventide::ipc::protocol;
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
|
||||
using eventide::ipc::lsp::PositionEncoding;
|
||||
using eventide::ipc::lsp::PositionMapper;
|
||||
using eventide::ipc::lsp::parse_position_encoding;
|
||||
using kota::ipc::lsp::PositionEncoding;
|
||||
using kota::ipc::lsp::PositionMapper;
|
||||
using kota::ipc::lsp::parse_position_encoding;
|
||||
|
||||
inline auto to_range(const PositionMapper& converter, LocalSourceRange range) -> protocol::Range {
|
||||
return protocol::Range{
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
#include "clang/AST/Attr.h"
|
||||
#include "clang/Basic/IdentifierTable.h"
|
||||
#include "clang/Basic/Module.h"
|
||||
|
||||
namespace clice::feature {
|
||||
|
||||
@@ -168,6 +169,7 @@ public:
|
||||
auto collect() -> std::vector<RawToken> {
|
||||
highlight_lexical(unit.interested_file());
|
||||
run();
|
||||
highlight_modules();
|
||||
merge_tokens();
|
||||
return std::move(tokens);
|
||||
}
|
||||
@@ -291,6 +293,58 @@ 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();
|
||||
@@ -345,10 +399,17 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -308,6 +308,10 @@ 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>()) {
|
||||
|
||||
@@ -131,33 +131,6 @@ 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:
|
||||
|
||||
@@ -33,19 +33,19 @@ void CompileGraph::ensure_resolved(std::uint32_t path_id) {
|
||||
}
|
||||
}
|
||||
|
||||
et::task<bool> CompileGraph::compile_deps(std::uint32_t path_id) {
|
||||
kota::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);
|
||||
}
|
||||
|
||||
et::task<bool> CompileGraph::compile(std::uint32_t path_id) {
|
||||
kota::task<bool> CompileGraph::compile(std::uint32_t path_id) {
|
||||
llvm::DenseSet<std::uint32_t> ancestors;
|
||||
co_return co_await compile_impl(path_id, ancestors);
|
||||
}
|
||||
|
||||
et::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
|
||||
llvm::DenseSet<std::uint32_t> ancestors,
|
||||
bool dispatch_self) {
|
||||
kota::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 @@ et::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
|
||||
co_return true;
|
||||
}
|
||||
|
||||
std::vector<et::task<bool>> dep_tasks;
|
||||
std::vector<kota::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 et::when_all(std::move(dep_tasks));
|
||||
auto results = co_await kota::when_all(std::move(dep_tasks));
|
||||
for(auto ok: results) {
|
||||
if(!ok) {
|
||||
co_return false;
|
||||
@@ -96,7 +96,7 @@ et::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<et::event>();
|
||||
it->second.completion = std::make_unique<kota::event>();
|
||||
|
||||
auto finish = [&, path_id] {
|
||||
auto& u = units.find(path_id)->second;
|
||||
@@ -113,17 +113,17 @@ et::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<et::task<bool, void, et::cancellation>> dep_tasks;
|
||||
std::vector<kota::task<bool, void, kota::cancellation>> dep_tasks;
|
||||
dep_tasks.reserve(deps.size());
|
||||
for(auto dep_id: deps) {
|
||||
dep_tasks.push_back(et::with_token(compile_impl(dep_id, ancestors), token));
|
||||
dep_tasks.push_back(kota::with_token(compile_impl(dep_id, ancestors), token));
|
||||
}
|
||||
|
||||
auto results = co_await et::when_all(std::move(dep_tasks));
|
||||
auto results = co_await kota::when_all(std::move(dep_tasks));
|
||||
|
||||
if(results.is_cancelled()) {
|
||||
finish();
|
||||
co_await et::cancel();
|
||||
co_await kota::cancel();
|
||||
}
|
||||
|
||||
for(auto ok: *results) {
|
||||
@@ -135,11 +135,11 @@ et::task<bool> CompileGraph::compile_impl(std::uint32_t path_id,
|
||||
}
|
||||
|
||||
// Dispatch the actual compilation, cancellable via the pre-captured token.
|
||||
auto result = co_await et::with_token(dispatch(path_id), token);
|
||||
auto result = co_await kota::with_token(dispatch(path_id), token);
|
||||
|
||||
if(!result.has_value()) {
|
||||
finish();
|
||||
co_await et::cancel();
|
||||
co_await kota::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<et::cancellation_source>();
|
||||
unit.source = std::make_unique<kota::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<et::cancellation_source>();
|
||||
unit.source = std::make_unique<kota::cancellation_source>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,16 +4,13 @@
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#include "eventide/async/async.h"
|
||||
|
||||
#include "kota/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;
|
||||
|
||||
@@ -33,14 +30,15 @@ struct CompileUnit {
|
||||
/// stale completions without ABA risk from raw-pointer comparison.
|
||||
std::uint64_t generation = 0;
|
||||
|
||||
std::unique_ptr<et::cancellation_source> source = std::make_unique<et::cancellation_source>();
|
||||
std::unique_ptr<et::event> completion;
|
||||
std::unique_ptr<kota::cancellation_source> source =
|
||||
std::make_unique<kota::cancellation_source>();
|
||||
std::unique_ptr<kota::event> completion;
|
||||
};
|
||||
|
||||
class CompileGraph {
|
||||
public:
|
||||
/// Performs the actual compilation (e.g. produce PCM file).
|
||||
using dispatch_fn = std::function<et::task<bool>(std::uint32_t path_id)>;
|
||||
using dispatch_fn = std::function<kota::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)>;
|
||||
@@ -48,11 +46,11 @@ public:
|
||||
CompileGraph(dispatch_fn dispatch, resolve_fn resolve);
|
||||
|
||||
/// Compile a unit and all its transitive dependencies.
|
||||
et::task<bool> compile(std::uint32_t path_id);
|
||||
kota::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.
|
||||
et::task<bool> compile_deps(std::uint32_t path_id);
|
||||
kota::task<bool> compile_deps(std::uint32_t path_id);
|
||||
|
||||
/// Mark path_id and all transitive dependents as dirty,
|
||||
/// cancelling any in-progress compilations.
|
||||
@@ -70,9 +68,9 @@ private:
|
||||
void ensure_resolved(std::uint32_t path_id);
|
||||
|
||||
/// Internal compile with ancestor tracking for cycle detection.
|
||||
et::task<bool> compile_impl(std::uint32_t path_id,
|
||||
llvm::DenseSet<std::uint32_t> ancestors,
|
||||
bool dispatch_self = true);
|
||||
kota::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
|
||||
|
||||
@@ -5,9 +5,6 @@
|
||||
#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"
|
||||
@@ -15,6 +12,9 @@
|
||||
#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 = eventide::ipc::lsp;
|
||||
using serde_raw = et::serde::RawValue;
|
||||
namespace lsp = kota::ipc::lsp;
|
||||
using serde_raw = kota::codec::RawValue;
|
||||
|
||||
/// Detect whether the cursor is inside a preamble directive (include/import).
|
||||
|
||||
Compiler::Compiler(et::event_loop& loop,
|
||||
et::ipc::JsonPeer& peer,
|
||||
Compiler::Compiler(kota::event_loop& loop,
|
||||
kota::ipc::JsonPeer& peer,
|
||||
Workspace& workspace,
|
||||
WorkerPool& pool,
|
||||
llvm::DenseMap<std::uint32_t, Session>& sessions) :
|
||||
@@ -47,8 +47,13 @@ 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);
|
||||
auto results =
|
||||
workspace.cdb.lookup(file_path, {.query_toolchain = true, .suppress_logging = true});
|
||||
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});
|
||||
if(results.empty())
|
||||
return {};
|
||||
|
||||
@@ -75,7 +80,7 @@ void Compiler::init_compile_graph() {
|
||||
};
|
||||
|
||||
// Dispatch: sends BuildPCM request to a stateless worker.
|
||||
auto dispatch = [this](std::uint32_t path_id) -> et::task<bool> {
|
||||
auto dispatch = [this](std::uint32_t path_id) -> kota::task<bool> {
|
||||
auto mod_it = workspace.path_to_module.find(path_id);
|
||||
if(mod_it == workspace.path_to_module.end())
|
||||
co_return false;
|
||||
@@ -97,7 +102,8 @@ 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.cache_dir, "cache", "pcm", pcm_filename);
|
||||
auto pcm_path =
|
||||
path::join(workspace.config.project.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()) {
|
||||
@@ -156,7 +162,11 @@ bool Compiler::fill_compile_args(llvm::StringRef path,
|
||||
}
|
||||
|
||||
// 2. Normal CDB lookup for the file itself.
|
||||
auto results = workspace.cdb.lookup(path, {.query_toolchain = true});
|
||||
// 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);
|
||||
if(!results.empty()) {
|
||||
auto& cmd = results.front();
|
||||
directory = cmd.resolved.directory.str();
|
||||
@@ -205,7 +215,13 @@ bool Compiler::fill_header_context_args(llvm::StringRef path,
|
||||
}
|
||||
|
||||
auto host_path = workspace.path_pool.resolve(ctx_ptr->host_path_id);
|
||||
auto host_results = workspace.cdb.lookup(host_path, {.query_toolchain = true});
|
||||
// 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});
|
||||
if(host_results.empty()) {
|
||||
LOG_WARN("fill_header_context_args: host {} has no CDB entry", host_path);
|
||||
return false;
|
||||
@@ -355,7 +371,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.cache_dir, "header_context");
|
||||
auto preamble_dir = path::join(workspace.config.project.cache_dir, "header_context");
|
||||
auto preamble_path = path::join(preamble_dir, preamble_filename);
|
||||
|
||||
if(!llvm::sys::fs::exists(preamble_path)) {
|
||||
@@ -393,10 +409,10 @@ std::string uri_to_path(const std::string& uri) {
|
||||
|
||||
void Compiler::publish_diagnostics(const std::string& uri,
|
||||
int version,
|
||||
const et::serde::RawValue& diagnostics_json) {
|
||||
const kota::codec::RawValue& diagnostics_json) {
|
||||
std::vector<protocol::Diagnostic> diagnostics;
|
||||
if(!diagnostics_json.empty()) {
|
||||
auto status = et::serde::json::from_json(diagnostics_json.data, diagnostics);
|
||||
auto status = kota::codec::json::from_json(diagnostics_json.data, diagnostics);
|
||||
if(!status) {
|
||||
LOG_WARN("Failed to deserialize diagnostics JSON for {}", uri);
|
||||
}
|
||||
@@ -415,9 +431,9 @@ void Compiler::clear_diagnostics(const std::string& uri) {
|
||||
peer.send_notification(params);
|
||||
}
|
||||
|
||||
et::task<bool> Compiler::ensure_pch(Session& session,
|
||||
const std::string& directory,
|
||||
const std::vector<std::string>& arguments) {
|
||||
kota::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;
|
||||
@@ -438,7 +454,7 @@ et::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.cache_dir,
|
||||
auto pch_path = path::join(workspace.config.project.cache_dir,
|
||||
"cache",
|
||||
"pch",
|
||||
std::format("{:016x}.pch", preamble_hash));
|
||||
@@ -471,9 +487,25 @@ et::task<bool> Compiler::ensure_pch(Session& session,
|
||||
}
|
||||
|
||||
// Register in-flight build so concurrent requests wait on us.
|
||||
auto completion = std::make_shared<et::event>();
|
||||
auto completion = std::make_shared<kota::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;
|
||||
@@ -502,6 +534,7 @@ et::task<bool> Compiler::ensure_pch(Session& session,
|
||||
st.bound = bound;
|
||||
st.hash = preamble_hash;
|
||||
st.deps = capture_deps_snapshot(workspace.path_pool, result.value().deps);
|
||||
st.document_links_json = std::move(result.value().pch_links_json);
|
||||
st.building.reset();
|
||||
|
||||
session.pch_ref = Session::PCHRef{path_id, preamble_hash, bound};
|
||||
@@ -518,11 +551,11 @@ et::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).
|
||||
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) {
|
||||
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) {
|
||||
auto path_id = session.path_id;
|
||||
|
||||
// Compile C++20 module dependencies (PCMs).
|
||||
@@ -619,7 +652,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.
|
||||
et::task<bool> Compiler::ensure_compiled(Session& session) {
|
||||
kota::task<bool> Compiler::ensure_compiled(Session& session) {
|
||||
auto path_id = session.path_id;
|
||||
|
||||
LOG_DEBUG("ensure_compiled: path_id={} version={} gen={} ast_dirty={}",
|
||||
@@ -662,7 +695,7 @@ et::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) -> et::task<> {
|
||||
std::shared_ptr<Session::PendingCompile> pc) -> kota::task<> {
|
||||
// Re-lookup session from the sessions map (pointer may have been
|
||||
// invalidated by DenseMap growth during co_await).
|
||||
auto find_session = [&]() -> Session* {
|
||||
@@ -893,7 +926,7 @@ Compiler::RawResult Compiler::handle_completion(const protocol::Position& positi
|
||||
item.kind = protocol::CompletionItemKind::File;
|
||||
items.push_back(std::move(item));
|
||||
}
|
||||
auto json = et::serde::json::to_json<et::ipc::lsp_config>(items);
|
||||
auto json = kota::codec::json::to_json<kota::ipc::lsp_config>(items);
|
||||
co_return serde_raw{json ? std::move(*json) : "[]"};
|
||||
}
|
||||
if(pctx.kind == CompletionContext::Import) {
|
||||
@@ -908,7 +941,7 @@ Compiler::RawResult Compiler::handle_completion(const protocol::Position& positi
|
||||
item.insert_text = name + ";";
|
||||
items.push_back(std::move(item));
|
||||
}
|
||||
auto json = et::serde::json::to_json<et::ipc::lsp_config>(items);
|
||||
auto json = kota::codec::json::to_json<kota::ipc::lsp_config>(items);
|
||||
co_return serde_raw{json ? std::move(*json) : "[]"};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,15 +8,16 @@
|
||||
#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"
|
||||
@@ -24,8 +25,7 @@
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace et = eventide;
|
||||
namespace protocol = et::ipc::protocol;
|
||||
namespace protocol = kota::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(et::event_loop& loop,
|
||||
et::ipc::JsonPeer& peer,
|
||||
Compiler(kota::event_loop& loop,
|
||||
kota::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.
|
||||
et::task<bool> ensure_compiled(Session& session);
|
||||
kota::task<bool> ensure_compiled(Session& session);
|
||||
|
||||
using RawResult = et::task<et::serde::RawValue, et::ipc::Error>;
|
||||
using RawResult = kota::task<kota::codec::RawValue, kota::ipc::Error>;
|
||||
|
||||
/// Forward a query to the stateful worker that holds this file's AST.
|
||||
/// Ensures compilation first. For position-sensitive queries (hover,
|
||||
@@ -97,20 +97,22 @@ public:
|
||||
std::function<void()> on_indexing_needed;
|
||||
|
||||
private:
|
||||
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_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_pch(Session& session,
|
||||
const std::string& directory,
|
||||
const std::vector<std::string>& arguments);
|
||||
kota::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 et::serde::RawValue& diags);
|
||||
void publish_diagnostics(const std::string& uri,
|
||||
int version,
|
||||
const kota::codec::RawValue& diags);
|
||||
|
||||
std::optional<HeaderFileContext> resolve_header_context(std::uint32_t header_path_id,
|
||||
Session* session);
|
||||
@@ -122,8 +124,8 @@ private:
|
||||
Session* session);
|
||||
|
||||
private:
|
||||
et::event_loop& loop;
|
||||
et::ipc::JsonPeer& peer;
|
||||
kota::event_loop& loop;
|
||||
kota::ipc::JsonPeer& peer;
|
||||
Workspace& workspace;
|
||||
WorkerPool& pool;
|
||||
llvm::DenseMap<std::uint32_t, Session>& sessions;
|
||||
|
||||
@@ -1,98 +1,194 @@
|
||||
#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.
|
||||
static void substitute_workspace(std::string& value, const std::string& 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;
|
||||
constexpr std::string_view placeholder = "${workspace}";
|
||||
std::string::size_type pos = 0;
|
||||
std::size_t pos = 0;
|
||||
while((pos = value.find(placeholder, pos)) != std::string::npos) {
|
||||
value.replace(pos, placeholder.size(), workspace_root);
|
||||
pos += workspace_root.size();
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
/// 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 {};
|
||||
}
|
||||
|
||||
if(index_dir.empty() && !cache_dir.empty()) {
|
||||
index_dir = path::join(cache_dir, "index");
|
||||
}
|
||||
// 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(logging_dir.empty() && !cache_dir.empty()) {
|
||||
logging_dir = path::join(cache_dir, "logs");
|
||||
if(auto ec = llvm::sys::fs::create_directories(dir)) {
|
||||
LOG_WARN("Failed to create XDG cache directory {}: {}", dir, ec.message());
|
||||
return {};
|
||||
}
|
||||
|
||||
// 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);
|
||||
return dir;
|
||||
}
|
||||
|
||||
std::optional<CliceConfig> CliceConfig::load(const std::string& path,
|
||||
const std::string& workspace_root) {
|
||||
auto content = fs::read(path);
|
||||
if(!content) {
|
||||
return std::nullopt;
|
||||
}
|
||||
void Config::apply_defaults(llvm::StringRef workspace_root) {
|
||||
auto& p = project;
|
||||
|
||||
auto result = eventide::serde::toml::parse<CliceConfig>(*content);
|
||||
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) {
|
||||
auto content = fs::read(path);
|
||||
if(!content)
|
||||
return std::nullopt;
|
||||
|
||||
auto result = kota::codec::toml::parse<Config>(*content);
|
||||
if(!result) {
|
||||
LOG_WARN("Failed to parse config file {}", path);
|
||||
LOG_ERROR("Invalid clice.toml {}: {}", path, result.error().to_string());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto config = std::move(*result);
|
||||
config.apply_defaults(workspace_root);
|
||||
|
||||
LOG_INFO("Loaded config from {}", path);
|
||||
return config;
|
||||
}
|
||||
|
||||
CliceConfig CliceConfig::load_from_workspace(const std::string& workspace_root) {
|
||||
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) {
|
||||
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)) {
|
||||
auto config = load(config_path, workspace_root);
|
||||
if(config)
|
||||
return std::move(*config);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// No config file found; use defaults
|
||||
CliceConfig config;
|
||||
Config config;
|
||||
config.apply_defaults(workspace_root);
|
||||
LOG_INFO(
|
||||
"No clice.toml found, using default configuration " "(stateful={}, stateless={}, memory_limit={}MB)",
|
||||
config.stateful_worker_count,
|
||||
config.stateless_worker_count,
|
||||
config.worker_memory_limit / (1024 * 1024));
|
||||
config.project.stateful_worker_count.value,
|
||||
config.project.stateless_worker_count.value,
|
||||
config.project.worker_memory_limit.value / (1024 * 1024));
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,44 +3,77 @@
|
||||
#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 {
|
||||
|
||||
/// 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
|
||||
using kota::meta::defaulted;
|
||||
|
||||
// Compilation database path (empty = auto-detect)
|
||||
std::string compile_commands_path;
|
||||
/// 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;
|
||||
};
|
||||
|
||||
// Cache directory (empty = default: <workspace>/.clice/)
|
||||
std::string cache_dir;
|
||||
/// Corresponds to the `[project]` section in clice.toml.
|
||||
struct ProjectConfig {
|
||||
defaulted<bool> clang_tidy = {};
|
||||
defaulted<int> max_active_file = {};
|
||||
|
||||
// Index storage directory (default: <cache_dir>/index/)
|
||||
std::string index_dir;
|
||||
defaulted<std::string> cache_dir;
|
||||
defaulted<std::string> index_dir;
|
||||
defaulted<std::string> logging_dir;
|
||||
|
||||
// Logging directory (default: <cache_dir>/logs/)
|
||||
std::string logging_dir;
|
||||
defaulted<std::vector<std::string>> compile_commands_paths;
|
||||
|
||||
// Background indexing
|
||||
bool enable_indexing = true;
|
||||
int idle_timeout_ms = 3000;
|
||||
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;
|
||||
|
||||
/// Compute default values for any field left at its zero/empty sentinel.
|
||||
void apply_defaults(const std::string& workspace_root);
|
||||
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;
|
||||
|
||||
/// Try to load configuration from a TOML file.
|
||||
/// 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);
|
||||
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);
|
||||
|
||||
/// Load config from the workspace, trying standard locations.
|
||||
/// Returns a default config (with apply_defaults) if no file is found.
|
||||
static CliceConfig load_from_workspace(const std::string& workspace_root);
|
||||
static Config load_from_workspace(llvm::StringRef workspace_root);
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
#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"
|
||||
@@ -15,6 +13,9 @@
|
||||
#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"
|
||||
@@ -22,7 +23,7 @@
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace lsp = eventide::ipc::lsp;
|
||||
namespace lsp = kota::ipc::lsp;
|
||||
|
||||
void Indexer::merge(const void* tu_index_data, std::size_t size) {
|
||||
auto tu_index = index::TUIndex::from(tu_index_data);
|
||||
@@ -624,19 +625,106 @@ 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.enable_indexing || indexing_active || indexing_scheduled)
|
||||
if(!*workspace.config.project.enable_indexing || indexing_active || indexing_scheduled)
|
||||
return;
|
||||
indexing_scheduled = true;
|
||||
|
||||
if(!index_idle_timer) {
|
||||
index_idle_timer = std::make_shared<et::timer>(et::timer::create(loop));
|
||||
index_idle_timer = std::make_shared<kota::timer>(kota::timer::create(loop));
|
||||
}
|
||||
index_idle_timer->start(std::chrono::milliseconds(workspace.config.idle_timeout_ms));
|
||||
index_idle_timer->start(std::chrono::milliseconds(*workspace.config.project.idle_timeout_ms));
|
||||
loop.schedule(run_background_indexing());
|
||||
}
|
||||
|
||||
et::task<> Indexer::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() {
|
||||
if(index_idle_timer) {
|
||||
co_await index_idle_timer->wait();
|
||||
}
|
||||
@@ -648,49 +736,89 @@ et::task<> Indexer::run_background_indexing() {
|
||||
}
|
||||
|
||||
indexing_active = true;
|
||||
std::size_t processed = 0;
|
||||
++monitor_generation;
|
||||
loop.schedule(monitor_resources(monitor_generation));
|
||||
|
||||
while(index_queue_pos < index_queue.size()) {
|
||||
auto server_path_id = index_queue[index_queue_pos];
|
||||
index_queue_pos++;
|
||||
// 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); });
|
||||
|
||||
auto file_path = std::string(workspace.path_pool.resolve(server_path_id));
|
||||
auto batch = index_queue.size() - index_queue_pos;
|
||||
std::size_t dispatched = 0;
|
||||
std::size_t completed = 0;
|
||||
finished = 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);
|
||||
// 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);
|
||||
} else {
|
||||
LOG_WARN("Background index IPC error for {}: {}", file_path, result.error().message);
|
||||
progress.reset();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
LOG_INFO("Background indexing complete: {} files processed", processed);
|
||||
save(workspace.config.index_dir);
|
||||
++monitor_generation; // Stop the monitor coroutine.
|
||||
LOG_INFO("Background indexing complete: {} files dispatched", dispatched);
|
||||
save(workspace.config.project.index_dir);
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -7,22 +7,23 @@
|
||||
#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 et = eventide;
|
||||
namespace protocol = et::ipc::protocol;
|
||||
namespace lsp = et::ipc::lsp;
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
namespace lsp = kota::ipc::lsp;
|
||||
|
||||
struct Session;
|
||||
class Compiler;
|
||||
@@ -54,7 +55,7 @@ struct SymbolInfo {
|
||||
/// - Document lifecycle — handled by MasterServer
|
||||
class Indexer {
|
||||
public:
|
||||
Indexer(et::event_loop& loop,
|
||||
Indexer(kota::event_loop& loop,
|
||||
Workspace& workspace,
|
||||
llvm::DenseMap<std::uint32_t, Session>& sessions,
|
||||
WorkerPool& pool,
|
||||
@@ -63,6 +64,47 @@ 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);
|
||||
|
||||
@@ -165,7 +207,7 @@ private:
|
||||
}
|
||||
|
||||
private:
|
||||
et::event_loop& loop;
|
||||
kota::event_loop& loop;
|
||||
Workspace& workspace;
|
||||
llvm::DenseMap<std::uint32_t, Session>& sessions;
|
||||
WorkerPool& pool;
|
||||
@@ -176,14 +218,40 @@ 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<et::timer> index_idle_timer;
|
||||
std::shared_ptr<kota::timer> index_idle_timer;
|
||||
|
||||
et::task<> run_background_indexing();
|
||||
/// 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);
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -6,37 +6,39 @@
|
||||
#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 = 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;
|
||||
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;
|
||||
|
||||
/// Serialize a value to a JSON RawValue using LSP config.
|
||||
template <typename T>
|
||||
static serde_raw to_raw(const T& value) {
|
||||
auto json = et::serde::json::to_json<et::ipc::lsp_config>(value);
|
||||
auto json = kota::codec::json::to_json<kota::ipc::lsp_config>(value);
|
||||
return serde_raw{json ? std::move(*json) : "null"};
|
||||
}
|
||||
|
||||
MasterServer::MasterServer(et::event_loop& loop, et::ipc::JsonPeer& peer, std::string self_path) :
|
||||
MasterServer::MasterServer(kota::event_loop& loop,
|
||||
kota::ipc::JsonPeer& peer,
|
||||
std::string self_path) :
|
||||
loop(loop), peer(peer), pool(loop), compiler(loop, peer, workspace, pool, sessions),
|
||||
indexer(loop,
|
||||
workspace,
|
||||
@@ -54,63 +56,90 @@ MasterServer::MasterServer(et::event_loop& loop, et::ipc::JsonPeer& peer, std::s
|
||||
|
||||
MasterServer::~MasterServer() = default;
|
||||
|
||||
et::task<> MasterServer::load_workspace() {
|
||||
void MasterServer::load_workspace() {
|
||||
if(workspace_root.empty())
|
||||
co_return;
|
||||
return;
|
||||
|
||||
if(!workspace.config.cache_dir.empty()) {
|
||||
auto ec = llvm::sys::fs::create_directories(workspace.config.cache_dir);
|
||||
auto& cfg = workspace.config.project;
|
||||
|
||||
if(!cfg.cache_dir.empty()) {
|
||||
auto ec = llvm::sys::fs::create_directories(cfg.cache_dir);
|
||||
if(ec) {
|
||||
LOG_WARN("Failed to create cache directory {}: {}",
|
||||
workspace.config.cache_dir,
|
||||
std::string_view(cfg.cache_dir),
|
||||
ec.message());
|
||||
} else {
|
||||
LOG_INFO("Cache directory: {}", workspace.config.cache_dir);
|
||||
LOG_INFO("Cache directory: {}", std::string_view(cfg.cache_dir));
|
||||
}
|
||||
|
||||
for(auto* subdir: {"cache/pch", "cache/pcm"}) {
|
||||
auto dir = path::join(workspace.config.cache_dir, subdir);
|
||||
auto ec2 = llvm::sys::fs::create_directories(dir);
|
||||
if(ec2) {
|
||||
auto dir = path::join(cfg.cache_dir, subdir);
|
||||
if(auto ec2 = llvm::sys::fs::create_directories(dir))
|
||||
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;
|
||||
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: {}",
|
||||
workspace.config.compile_commands_path);
|
||||
}
|
||||
}
|
||||
|
||||
if(cdb_path.empty()) {
|
||||
for(auto* subdir: {"build", "cmake-build-debug", "cmake-build-release", "out", "."}) {
|
||||
auto candidate = path::join(workspace_root, subdir, "compile_commands.json");
|
||||
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;
|
||||
} else {
|
||||
LOG_WARN("Configured compile_commands_path not found: {}", configured);
|
||||
}
|
||||
}
|
||||
|
||||
// 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");
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(cdb_path.empty()) {
|
||||
LOG_WARN("No compile_commands.json found in workspace {}", workspace_root);
|
||||
co_return;
|
||||
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);
|
||||
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);
|
||||
});
|
||||
workspace.dep_graph.build_reverse_map();
|
||||
|
||||
auto unresolved = report.includes_found - report.includes_resolved;
|
||||
@@ -129,14 +158,13 @@ et::task<> 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(workspace.config.index_dir);
|
||||
indexer.load(cfg.index_dir);
|
||||
|
||||
if(workspace.config.enable_indexing) {
|
||||
if(*cfg.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);
|
||||
@@ -154,7 +182,7 @@ void MasterServer::register_handlers() {
|
||||
peer.on_request([this](RequestContext& ctx, const protocol::InitializeParams& params)
|
||||
-> RequestResult<protocol::InitializeParams> {
|
||||
if(lifecycle != ServerLifecycle::Uninitialized) {
|
||||
co_return et::outcome_error(protocol::Error{"Server already initialized"});
|
||||
co_return kota::outcome_error(protocol::Error{"Server already initialized"});
|
||||
}
|
||||
|
||||
auto& init = params.lsp__initialize_params;
|
||||
@@ -162,6 +190,14 @@ 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);
|
||||
|
||||
@@ -242,27 +278,47 @@ void MasterServer::register_handlers() {
|
||||
});
|
||||
|
||||
peer.on_notification([this](const protocol::InitializedParams& params) {
|
||||
workspace.config = CliceConfig::load_from_workspace(workspace_root);
|
||||
// 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.
|
||||
workspace.config = Config::load_from_workspace(workspace_root);
|
||||
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());
|
||||
} 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();
|
||||
}
|
||||
|
||||
if(!workspace.config.logging_dir.empty()) {
|
||||
auto& cfg = workspace.config.project;
|
||||
|
||||
if(!cfg.logging_dir.empty()) {
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto pid = llvm::sys::Process::getProcessId();
|
||||
auto session_dir = path::join(workspace.config.logging_dir,
|
||||
std::format("{:%Y-%m-%d_%H-%M-%S}_{}", now, pid));
|
||||
auto session_dir =
|
||||
path::join(cfg.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)",
|
||||
workspace.config.stateful_worker_count,
|
||||
workspace.config.stateless_worker_count,
|
||||
workspace.config.idle_timeout_ms);
|
||||
cfg.stateful_worker_count.value,
|
||||
cfg.stateless_worker_count.value,
|
||||
*cfg.idle_timeout_ms);
|
||||
|
||||
WorkerPoolOptions pool_opts;
|
||||
pool_opts.self_path = self_path;
|
||||
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.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.log_dir = session_log_dir;
|
||||
if(!pool.start(pool_opts)) {
|
||||
LOG_ERROR("Failed to start worker pool");
|
||||
@@ -275,7 +331,10 @@ void MasterServer::register_handlers() {
|
||||
indexer.schedule();
|
||||
};
|
||||
|
||||
loop.schedule(load_workspace());
|
||||
indexer.set_peer(&peer);
|
||||
indexer.set_max_concurrency(cfg.stateless_worker_count.value);
|
||||
|
||||
load_workspace();
|
||||
});
|
||||
|
||||
peer.on_request(
|
||||
@@ -290,10 +349,10 @@ void MasterServer::register_handlers() {
|
||||
lifecycle = ServerLifecycle::Exited;
|
||||
LOG_INFO("Exit notification received");
|
||||
|
||||
indexer.save(workspace.config.index_dir);
|
||||
indexer.save(workspace.config.project.index_dir);
|
||||
workspace.save_cache();
|
||||
|
||||
loop.schedule([this]() -> et::task<> {
|
||||
loop.schedule([this]() -> kota::task<> {
|
||||
co_await pool.stop();
|
||||
loop.stop();
|
||||
}());
|
||||
@@ -478,15 +537,38 @@ void MasterServer::register_handlers() {
|
||||
co_return co_await compiler.forward_query(worker::QueryKind::DocumentSymbol, sit->second);
|
||||
});
|
||||
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx, const protocol::DocumentLinkParams& params) -> RawResult {
|
||||
auto path = uri_to_path(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_query(worker::QueryKind::DocumentLink, sit->second);
|
||||
});
|
||||
peer.on_request([this](RequestContext& ctx,
|
||||
const protocol::DocumentLinkParams& params) -> RawResult {
|
||||
auto path = uri_to_path(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"};
|
||||
auto& session = sit->second;
|
||||
auto result = co_await compiler.forward_query(worker::QueryKind::DocumentLink, session);
|
||||
if(!result.has_value())
|
||||
co_return serde_raw{"null"};
|
||||
// Merge document links from PCH if available.
|
||||
auto& links = result.value();
|
||||
// Re-lookup session after co_await since iterators may be invalidated.
|
||||
auto sit2 = sessions.find(path_id);
|
||||
if(sit2 != sessions.end() && sit2->second.pch_ref) {
|
||||
auto pch_it = workspace.pch_cache.find(sit2->second.pch_ref->path_id);
|
||||
if(pch_it != workspace.pch_cache.end() && !pch_it->second.document_links_json.empty()) {
|
||||
auto& pch_json = pch_it->second.document_links_json;
|
||||
// Merge two JSON arrays.
|
||||
if(!links.data.empty() && links.data != "null" && links.data.size() > 2) {
|
||||
// "[a,b]" + "[c,d]" -> "[a,b,c,d]"
|
||||
links.data.pop_back(); // remove trailing ']'
|
||||
links.data += ',';
|
||||
links.data.append(pch_json.begin() + 1, pch_json.end()); // skip '['
|
||||
} else {
|
||||
links.data = pch_json;
|
||||
}
|
||||
}
|
||||
}
|
||||
co_return std::move(links);
|
||||
});
|
||||
|
||||
peer.on_request(
|
||||
[this](RequestContext& ctx, const protocol::CodeActionParams& params) -> RawResult {
|
||||
@@ -591,28 +673,33 @@ 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_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 {
|
||||
[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_return serde_raw{"null"};
|
||||
co_return co_await compiler.forward_build(worker::BuildKind::SignatureHelp,
|
||||
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 {
|
||||
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"};
|
||||
auto pause = indexer.scoped_pause();
|
||||
auto result = 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.
|
||||
|
||||
|
||||
@@ -5,21 +5,19 @@
|
||||
#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,
|
||||
@@ -44,14 +42,14 @@ enum class ServerLifecycle : std::uint8_t {
|
||||
/// point to disk files. The only path from Session to Workspace is didSave.
|
||||
class MasterServer {
|
||||
public:
|
||||
MasterServer(et::event_loop& loop, et::ipc::JsonPeer& peer, std::string self_path);
|
||||
MasterServer(kota::event_loop& loop, kota::ipc::JsonPeer& peer, std::string self_path);
|
||||
~MasterServer();
|
||||
|
||||
void register_handlers();
|
||||
|
||||
private:
|
||||
et::event_loop& loop;
|
||||
et::ipc::JsonPeer& peer;
|
||||
kota::event_loop& loop;
|
||||
kota::ipc::JsonPeer& peer;
|
||||
|
||||
/// Persistent project-wide state (config, CDB, path pool, dependency
|
||||
/// graphs, compilation caches, symbol index).
|
||||
@@ -73,10 +71,11 @@ 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.
|
||||
|
||||
et::task<> load_workspace();
|
||||
void load_workspace();
|
||||
|
||||
using RawResult = et::task<et::serde::RawValue, et::ipc::Error>;
|
||||
using RawResult = kota::task<kota::codec::RawValue, kota::ipc::Error>;
|
||||
};
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -7,14 +7,15 @@
|
||||
#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 = eventide::ipc::protocol;
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
|
||||
/// Kind of AST query dispatched to a stateful worker.
|
||||
enum class QueryKind : uint8_t {
|
||||
@@ -51,7 +52,7 @@ struct CompileParams {
|
||||
struct CompileResult {
|
||||
int version;
|
||||
/// Diagnostics serialized as JSON (RawValue) to avoid bincode/serde annotation conflicts.
|
||||
eventide::serde::RawValue diagnostics;
|
||||
kota::codec::RawValue diagnostics;
|
||||
std::size_t memory_usage;
|
||||
std::vector<std::string> deps;
|
||||
/// Serialized TUIndex for the main file (interested_only=true).
|
||||
@@ -102,7 +103,8 @@ struct BuildResult {
|
||||
std::string output_path; ///< PCH or PCM path
|
||||
std::vector<std::string> deps;
|
||||
std::string tu_index_data;
|
||||
eventide::serde::RawValue result_json; ///< Completion/SignatureHelp result
|
||||
std::string pch_links_json; ///< Pre-serialized DocumentLink[] from PCH
|
||||
kota::codec::RawValue result_json; ///< Completion/SignatureHelp result
|
||||
};
|
||||
|
||||
struct DocumentUpdateParams {
|
||||
@@ -157,7 +159,7 @@ struct SwitchContextResult {
|
||||
|
||||
} // namespace clice::ext
|
||||
|
||||
namespace eventide::ipc::protocol {
|
||||
namespace kota::ipc::protocol {
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::CompileParams> {
|
||||
@@ -167,7 +169,7 @@ struct RequestTraits<clice::worker::CompileParams> {
|
||||
|
||||
template <>
|
||||
struct RequestTraits<clice::worker::QueryParams> {
|
||||
using Result = eventide::serde::RawValue;
|
||||
using Result = kota::codec::RawValue;
|
||||
constexpr inline static std::string_view method = "clice/worker/query";
|
||||
};
|
||||
|
||||
@@ -192,4 +194,4 @@ struct NotificationTraits<clice::worker::EvictedParams> {
|
||||
constexpr inline static std::string_view method = "clice/worker/evicted";
|
||||
};
|
||||
|
||||
} // namespace eventide::ipc::protocol
|
||||
} // namespace kota::ipc::protocol
|
||||
|
||||
@@ -5,15 +5,13 @@
|
||||
#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.
|
||||
@@ -45,7 +43,7 @@ struct Session {
|
||||
/// Other queries wait on the event; the compilation task itself
|
||||
/// runs independently and cannot be cancelled by LSP $/cancelRequest.
|
||||
struct PendingCompile {
|
||||
et::event done;
|
||||
kota::event done;
|
||||
bool succeeded = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
namespace et = eventide;
|
||||
using et::ipc::RequestResult;
|
||||
using RequestContext = et::ipc::BincodePeer::RequestContext;
|
||||
using kota::ipc::RequestResult;
|
||||
using RequestContext = kota::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.
|
||||
et::event ast_ready{false};
|
||||
kota::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
|
||||
et::mutex strand;
|
||||
kota::mutex strand;
|
||||
};
|
||||
|
||||
class StatefulWorker {
|
||||
et::ipc::BincodePeer& peer;
|
||||
kota::ipc::BincodePeer& peer;
|
||||
std::uint64_t memory_limit;
|
||||
|
||||
llvm::StringMap<std::shared_ptr<DocumentEntry>> documents;
|
||||
@@ -91,10 +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>
|
||||
et::task<et::serde::RawValue> with_ast(llvm::StringRef path, F&& fn) {
|
||||
kota::task<kota::codec::RawValue> with_ast(llvm::StringRef path, F&& fn) {
|
||||
auto it = documents.find(path);
|
||||
if(it == documents.end()) {
|
||||
co_return et::serde::RawValue{"null"};
|
||||
co_return kota::codec::RawValue{"null"};
|
||||
}
|
||||
|
||||
// Hold shared_ptr so Evict can't destroy the entry mid-request.
|
||||
@@ -104,9 +104,9 @@ class StatefulWorker {
|
||||
co_await doc->ast_ready.wait();
|
||||
co_await doc->strand.lock();
|
||||
|
||||
auto result = co_await et::queue([&]() -> et::serde::RawValue {
|
||||
auto result = co_await kota::queue([&]() -> kota::codec::RawValue {
|
||||
if(!doc->has_ast || (!doc->unit.completed() && !doc->unit.fatal_error()))
|
||||
return et::serde::RawValue{"null"};
|
||||
return kota::codec::RawValue{"null"};
|
||||
return fn(*doc);
|
||||
});
|
||||
|
||||
@@ -115,7 +115,7 @@ class StatefulWorker {
|
||||
}
|
||||
|
||||
public:
|
||||
StatefulWorker(et::ipc::BincodePeer& peer, std::uint64_t memory_limit) :
|
||||
StatefulWorker(kota::ipc::BincodePeer& peer, std::uint64_t memory_limit) :
|
||||
peer(peer), memory_limit(memory_limit) {}
|
||||
|
||||
void register_handlers();
|
||||
@@ -147,7 +147,7 @@ void StatefulWorker::register_handlers() {
|
||||
doc->pcms.try_emplace(name, pcm_path);
|
||||
}
|
||||
|
||||
auto compile_result = co_await et::queue([&]() -> worker::CompileResult {
|
||||
auto compile_result = co_await kota::queue([&]() -> worker::CompileResult {
|
||||
ScopedTimer timer;
|
||||
|
||||
CompilationParams cp;
|
||||
@@ -169,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 = et::serde::json::to_json<et::ipc::lsp_config>(diags);
|
||||
result.diagnostics = et::serde::RawValue{json ? std::move(*json) : "[]"};
|
||||
auto json = kota::codec::json::to_json<kota::ipc::lsp_config>(diags);
|
||||
result.diagnostics = kota::codec::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 = et::serde::RawValue{"[]"};
|
||||
result.diagnostics = kota::codec::RawValue{"[]"};
|
||||
LOG_WARN("Compile incomplete: path={}, {}ms", params.path, timer.ms());
|
||||
}
|
||||
result.memory_usage = 0; // TODO: query actual memory
|
||||
@@ -201,7 +201,7 @@ void StatefulWorker::register_handlers() {
|
||||
|
||||
// === DocumentUpdate ===
|
||||
// Only mark the document dirty — do NOT update doc.text or doc.version
|
||||
// here. The et::queue compilation work may be reading doc.text on the
|
||||
// here. The kota::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.
|
||||
@@ -238,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) : et::serde::RawValue{"null"};
|
||||
return result ? to_raw(*result) : kota::codec::RawValue{"null"};
|
||||
});
|
||||
case K::GoToDefinition:
|
||||
// TODO: Implement go-to-definition
|
||||
co_return et::serde::RawValue{"[]"};
|
||||
co_return kota::codec::RawValue{"[]"};
|
||||
case K::SemanticTokens:
|
||||
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
|
||||
return to_raw(feature::semantic_tokens(doc.unit));
|
||||
@@ -268,9 +268,9 @@ void StatefulWorker::register_handlers() {
|
||||
});
|
||||
case K::CodeAction:
|
||||
// TODO: Implement code actions
|
||||
co_return et::serde::RawValue{"[]"};
|
||||
co_return kota::codec::RawValue{"[]"};
|
||||
}
|
||||
co_return et::serde::RawValue{"null"};
|
||||
co_return kota::codec::RawValue{"null"};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -284,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));
|
||||
|
||||
et::event_loop loop;
|
||||
kota::event_loop loop;
|
||||
|
||||
auto transport_result = et::ipc::StreamTransport::open_stdio(loop);
|
||||
auto transport_result = kota::ipc::StreamTransport::open_stdio(loop);
|
||||
if(!transport_result) {
|
||||
LOG_ERROR("Failed to open stdio transport");
|
||||
return 1;
|
||||
}
|
||||
|
||||
et::ipc::BincodePeer peer(loop, std::move(*transport_result));
|
||||
kota::ipc::BincodePeer peer(loop, std::move(*transport_result));
|
||||
|
||||
StatefulWorker worker(peer, memory_limit);
|
||||
worker.register_handlers();
|
||||
|
||||
@@ -1,22 +1,38 @@
|
||||
#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 {
|
||||
|
||||
namespace et = eventide;
|
||||
using et::ipc::RequestResult;
|
||||
using RequestContext = et::ipc::BincodePeer::RequestContext;
|
||||
/// 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;
|
||||
|
||||
/// Extract error messages from compilation diagnostics.
|
||||
static std::string collect_errors(CompilationUnit& unit) {
|
||||
@@ -96,8 +112,13 @@ static worker::BuildResult handle_build_pch(const worker::BuildParams& params) {
|
||||
errors = collect_errors(unit);
|
||||
|
||||
std::string tu_index_data;
|
||||
if(success)
|
||||
std::string pch_links_json;
|
||||
if(success) {
|
||||
tu_index_data = serialize_tu_index(unit);
|
||||
auto links = feature::document_links(unit);
|
||||
auto raw = to_raw(links);
|
||||
pch_links_json = std::move(raw.data);
|
||||
}
|
||||
|
||||
// Destroy CompilationUnit to flush PCH to disk.
|
||||
unit = CompilationUnit(nullptr);
|
||||
@@ -110,6 +131,7 @@ static worker::BuildResult handle_build_pch(const worker::BuildParams& params) {
|
||||
result.output_path = std::move(final_path);
|
||||
result.deps = pch_info.deps;
|
||||
result.tu_index_data = std::move(tu_index_data);
|
||||
result.pch_links_json = std::move(pch_links_json);
|
||||
return result;
|
||||
} else {
|
||||
LOG_WARN("BuildPCH failed: file={}, {}ms, errors=[{}]", params.file, timer.ms(), errors);
|
||||
@@ -260,24 +282,27 @@ int run_stateless_worker_mode(const std::string& worker_name, const std::string&
|
||||
|
||||
LOG_INFO("Starting stateless worker");
|
||||
|
||||
et::event_loop loop;
|
||||
kota::event_loop loop;
|
||||
|
||||
auto transport_result = et::ipc::StreamTransport::open_stdio(loop);
|
||||
auto transport_result = kota::ipc::StreamTransport::open_stdio(loop);
|
||||
if(!transport_result) {
|
||||
LOG_ERROR("Failed to open stdio transport");
|
||||
return 1;
|
||||
}
|
||||
|
||||
et::ipc::BincodePeer peer(loop, std::move(*transport_result));
|
||||
kota::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 et::queue([&]() -> worker::BuildResult {
|
||||
auto result = co_await kota::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: return handle_index(params);
|
||||
case K::Index: {
|
||||
ScopedNice guard;
|
||||
return handle_index(params);
|
||||
}
|
||||
case K::Completion: return handle_completion(params);
|
||||
case K::SignatureHelp: return handle_signature_help(params);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
#include <vector>
|
||||
|
||||
#include "compile/compilation.h"
|
||||
#include "eventide/ipc/json_codec.h"
|
||||
#include "eventide/serde/json/serializer.h"
|
||||
#include "eventide/serde/serde/raw_value.h"
|
||||
|
||||
#include "kota/codec/json/json.h"
|
||||
#include "kota/ipc/codec/json.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 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"};
|
||||
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"};
|
||||
}
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -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, etc.) that bypasses spdlog.
|
||||
et::task<> drain_stderr(et::pipe stderr_pipe, std::string prefix) {
|
||||
/// (crash stacktraces, assertion failures, sanitizer reports, etc.).
|
||||
kota::task<> drain_stderr(kota::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 @@ et::task<> drain_stderr(et::pipe stderr_pipe, std::string prefix) {
|
||||
break;
|
||||
auto line = buffer.substr(pos, nl - pos);
|
||||
if(!line.empty()) {
|
||||
LOG_DEBUG("{} {}", prefix, line);
|
||||
LOG_WARN("{} {}", prefix, line);
|
||||
}
|
||||
pos = nl + 1;
|
||||
}
|
||||
@@ -41,7 +41,7 @@ et::task<> drain_stderr(et::pipe stderr_pipe, std::string prefix) {
|
||||
}
|
||||
|
||||
if(!buffer.empty()) {
|
||||
LOG_DEBUG("{} {}", prefix, buffer);
|
||||
LOG_WARN("{} {}", 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);
|
||||
|
||||
et::process::options opts;
|
||||
kota::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 = {
|
||||
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
|
||||
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
|
||||
};
|
||||
|
||||
auto result = et::process::spawn(opts, loop);
|
||||
auto result = kota::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<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));
|
||||
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));
|
||||
|
||||
// Schedule stderr log collection
|
||||
std::string prefix = "[" + worker_name + "]";
|
||||
@@ -107,24 +107,29 @@ 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
|
||||
@@ -142,31 +147,26 @@ bool WorkerPool::start(const WorkerPoolOptions& options) {
|
||||
return true;
|
||||
}
|
||||
|
||||
et::task<> WorkerPool::stop() {
|
||||
kota::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 to all workers
|
||||
for(auto& w: stateless_workers) {
|
||||
// Send SIGTERM. monitor_worker coroutines handle the wait.
|
||||
for(auto& w: stateless_workers)
|
||||
w.proc.kill(SIGTERM);
|
||||
}
|
||||
for(auto& w: stateful_workers) {
|
||||
for(auto& w: stateful_workers)
|
||||
w.proc.kill(SIGTERM);
|
||||
}
|
||||
|
||||
// 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();
|
||||
// Wait until all monitor_worker coroutines have finished.
|
||||
if(alive_count_ > 0) {
|
||||
all_exited_.reset();
|
||||
co_await all_exited_.wait();
|
||||
}
|
||||
|
||||
LOG_INFO("WorkerPool stopped");
|
||||
@@ -197,7 +197,10 @@ 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].owned_documents < stateful_workers[best].owned_documents) {
|
||||
if(!stateful_workers[i].alive)
|
||||
continue;
|
||||
if(!stateful_workers[best].alive ||
|
||||
stateful_workers[i].owned_documents < stateful_workers[best].owned_documents) {
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
@@ -232,4 +235,127 @@ 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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
namespace et = eventide;
|
||||
using et::ipc::RequestResult;
|
||||
using kota::ipc::RequestResult;
|
||||
|
||||
struct WorkerPoolOptions {
|
||||
std::string self_path;
|
||||
@@ -28,23 +28,24 @@ struct WorkerPoolOptions {
|
||||
|
||||
class WorkerPool {
|
||||
public:
|
||||
WorkerPool(et::event_loop& loop) : loop(loop) {}
|
||||
WorkerPool(kota::event_loop& loop) : loop(loop) {}
|
||||
|
||||
/// Spawn all worker processes. Returns false on failure.
|
||||
bool start(const WorkerPoolOptions& options);
|
||||
|
||||
/// Gracefully stop all workers.
|
||||
et::task<> stop();
|
||||
kota::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,
|
||||
et::ipc::request_options opts = {});
|
||||
kota::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, et::ipc::request_options opts = {});
|
||||
RequestResult<Params> send_stateless(const Params& params,
|
||||
kota::ipc::request_options opts = {});
|
||||
|
||||
/// Send a notification to the stateful worker owning path_id (if any).
|
||||
template <typename Params>
|
||||
@@ -60,12 +61,14 @@ public:
|
||||
|
||||
private:
|
||||
struct WorkerProcess {
|
||||
et::process proc;
|
||||
std::unique_ptr<et::ipc::BincodePeer> peer;
|
||||
kota::process proc;
|
||||
std::unique_ptr<kota::ipc::BincodePeer> peer;
|
||||
std::size_t owned_documents = 0;
|
||||
bool alive = true;
|
||||
unsigned restart_count = 0;
|
||||
};
|
||||
|
||||
et::event_loop& loop;
|
||||
kota::event_loop& loop;
|
||||
llvm::SmallVector<WorkerProcess> stateless_workers;
|
||||
llvm::SmallVector<WorkerProcess> stateful_workers;
|
||||
std::size_t next_stateless = 0;
|
||||
@@ -79,34 +82,51 @@ 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,
|
||||
et::ipc::request_options opts) {
|
||||
kota::ipc::request_options opts) {
|
||||
if(stateful_workers.empty()) {
|
||||
co_return et::outcome_error(et::ipc::Error{"No stateful workers available"});
|
||||
co_return kota::outcome_error(kota::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,
|
||||
et::ipc::request_options opts) {
|
||||
kota::ipc::request_options opts) {
|
||||
if(stateless_workers.empty()) {
|
||||
co_return et::outcome_error(et::ipc::Error{"No stateless workers available"});
|
||||
co_return kota::outcome_error(kota::ipc::Error{"No stateless workers available"});
|
||||
}
|
||||
auto idx = next_stateless;
|
||||
next_stateless = (next_stateless + 1) % stateless_workers.size();
|
||||
co_return co_await stateless_workers[idx].peer->send_request(params, opts);
|
||||
// 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"});
|
||||
}
|
||||
|
||||
template <typename Params>
|
||||
@@ -114,6 +134,8 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = eventide::ipc::lsp;
|
||||
namespace lsp = kota::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.cache_dir.empty())
|
||||
if(config.project.cache_dir.empty())
|
||||
return;
|
||||
|
||||
auto cache_path = path::join(config.cache_dir, "cache", "cache.json");
|
||||
auto cache_path = path::join(config.project.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 = eventide::serde::json::from_json(*content, data);
|
||||
auto status = kota::codec::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.cache_dir, "cache", "pch", entry.filename);
|
||||
auto pch_path = path::join(config.project.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.cache_dir, "cache", "pcm", entry.filename);
|
||||
auto pcm_path = path::join(config.project.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.cache_dir.empty())
|
||||
if(config.project.cache_dir.empty())
|
||||
return;
|
||||
|
||||
CacheData data;
|
||||
@@ -300,13 +300,13 @@ void Workspace::save_cache() {
|
||||
data.pcm.push_back(std::move(entry));
|
||||
}
|
||||
|
||||
auto json_str = eventide::serde::json::to_json(data);
|
||||
auto json_str = kota::codec::json::to_json(data);
|
||||
if(!json_str) {
|
||||
LOG_WARN("Failed to serialize cache.json");
|
||||
return;
|
||||
}
|
||||
|
||||
auto cache_path = path::join(config.cache_dir, "cache", "cache.json");
|
||||
auto cache_path = path::join(config.project.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.cache_dir.empty())
|
||||
if(config.project.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.cache_dir, subdir);
|
||||
auto dir = path::join(config.project.cache_dir, subdir);
|
||||
std::error_code ec;
|
||||
for(auto it = llvm::sys::fs::directory_iterator(dir, ec);
|
||||
!ec && it != llvm::sys::fs::directory_iterator();
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
#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"
|
||||
@@ -18,6 +16,8 @@
|
||||
#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,9 +25,8 @@
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace et = eventide;
|
||||
namespace protocol = et::ipc::protocol;
|
||||
namespace lsp = et::ipc::lsp;
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
namespace lsp = kota::ipc::lsp;
|
||||
|
||||
/// Two-layer staleness snapshot for compilation artifacts (PCH, AST, etc.).
|
||||
///
|
||||
@@ -140,7 +139,8 @@ struct PCHState {
|
||||
std::uint32_t bound = 0;
|
||||
std::uint64_t hash = 0;
|
||||
DepsSnapshot deps;
|
||||
std::shared_ptr<eventide::event> building;
|
||||
std::string document_links_json; ///< Pre-serialized DocumentLink[] from PCH build
|
||||
std::shared_ptr<kota::event> building;
|
||||
};
|
||||
|
||||
/// Cached PCM state for a single C++20 module. Shared across all files that
|
||||
@@ -170,7 +170,7 @@ struct PCMState {
|
||||
/// - didSave (on_file_saved: rescan disk, cascade invalidation)
|
||||
/// - Background index (merge TUIndex results from stateless workers)
|
||||
struct Workspace {
|
||||
CliceConfig config;
|
||||
Config config;
|
||||
CompilationDatabase cdb;
|
||||
|
||||
PathPool path_pool;
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
#include <system_error>
|
||||
#include <type_traits>
|
||||
|
||||
#include "eventide/common/meta.h"
|
||||
#include "eventide/common/ranges.h"
|
||||
#include "eventide/reflection/enum.h"
|
||||
#include "eventide/reflection/struct.h"
|
||||
|
||||
#include "kota/meta/enum.h"
|
||||
#include "kota/meta/struct.h"
|
||||
#include "kota/support/ranges.h"
|
||||
#include "kota/support/type_traits.h"
|
||||
#include "llvm/ADT/SmallString.h"
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
#include "llvm/Support/Error.h"
|
||||
@@ -86,7 +85,7 @@ struct std::formatter<std::error_code> : std::formatter<std::string_view> {
|
||||
}
|
||||
};
|
||||
|
||||
template <eventide::refl::enum_type E>
|
||||
template <kota::meta::enum_type E>
|
||||
struct std::formatter<E> : std::formatter<std::string> {
|
||||
using Base = std::formatter<std::string>;
|
||||
|
||||
@@ -97,7 +96,7 @@ struct std::formatter<E> : std::formatter<std::string> {
|
||||
|
||||
template <typename FormatContext>
|
||||
auto format(const E& value, FormatContext& ctx) const {
|
||||
auto name = eventide::refl::enum_name(value);
|
||||
auto name = kota::meta::enum_name(value);
|
||||
if(name.empty()) {
|
||||
using U = std::underlying_type_t<E>;
|
||||
return Base::format(std::format("{}", static_cast<U>(value)), ctx);
|
||||
@@ -107,9 +106,8 @@ struct std::formatter<E> : std::formatter<std::string> {
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept clice_reflectable_class =
|
||||
eventide::refl::reflectable_class<T> && !eventide::sequence_range<T> &&
|
||||
!eventide::set_range<T> && !eventide::map_range<T>;
|
||||
concept clice_reflectable_class = kota::meta::reflectable_class<T> && !kota::sequence_range<T> &&
|
||||
!kota::set_range<T> && !kota::map_range<T>;
|
||||
|
||||
template <clice_reflectable_class T>
|
||||
struct std::formatter<T> : std::formatter<std::string> {
|
||||
@@ -138,7 +136,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(eventide::map_range<T>) {
|
||||
} else if constexpr(kota::map_range<T>) {
|
||||
std::string result = "{";
|
||||
bool first = true;
|
||||
for(auto&& [key, value]: object) {
|
||||
@@ -150,8 +148,8 @@ std::string dump(const Object& object) {
|
||||
}
|
||||
result += "}";
|
||||
return result;
|
||||
} else if constexpr(eventide::set_range<T> || eventide::sequence_range<T>) {
|
||||
std::string result = eventide::set_range<T> ? "{" : "[";
|
||||
} else if constexpr(kota::set_range<T> || kota::sequence_range<T>) {
|
||||
std::string result = kota::set_range<T> ? "{" : "[";
|
||||
bool first = true;
|
||||
for(auto&& value: object) {
|
||||
if(!first) {
|
||||
@@ -160,10 +158,10 @@ std::string dump(const Object& object) {
|
||||
first = false;
|
||||
result += dump(value);
|
||||
}
|
||||
result += eventide::set_range<T> ? "}" : "]";
|
||||
result += kota::set_range<T> ? "}" : "]";
|
||||
return result;
|
||||
} else if constexpr(eventide::refl::enum_type<T>) {
|
||||
auto name = eventide::refl::enum_name(object);
|
||||
} else if constexpr(kota::meta::enum_type<T>) {
|
||||
auto name = kota::meta::enum_name(object);
|
||||
if(!name.empty()) {
|
||||
return std::format("\"{}\"", name);
|
||||
}
|
||||
@@ -172,7 +170,7 @@ std::string dump(const Object& object) {
|
||||
} else if constexpr(clice_reflectable_class<T>) {
|
||||
std::string result = "{";
|
||||
bool first = true;
|
||||
eventide::refl::for_each(object, [&](auto field) {
|
||||
kota::meta::for_each(object, [&](auto field) {
|
||||
if(!first) {
|
||||
result += ", ";
|
||||
}
|
||||
@@ -181,7 +179,7 @@ std::string dump(const Object& object) {
|
||||
});
|
||||
result += "}";
|
||||
return result;
|
||||
} else if constexpr(eventide::Formattable<T>) {
|
||||
} else if constexpr(kota::Formattable<T>) {
|
||||
return std::format("{}", object);
|
||||
} else {
|
||||
return "<unformattable>";
|
||||
|
||||
@@ -289,7 +289,7 @@ std::expected<GlobPattern::SubGlobPattern, std::string>
|
||||
return pat;
|
||||
}
|
||||
|
||||
bool GlobPattern::match(llvm::StringRef str) {
|
||||
bool GlobPattern::match(llvm::StringRef str) const {
|
||||
if(!str.consume_front(prefix)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ public:
|
||||
}
|
||||
|
||||
/// \returns \p true if \p str matches this glob pattern
|
||||
bool match(llvm::StringRef s);
|
||||
bool match(llvm::StringRef s) const;
|
||||
|
||||
private:
|
||||
/// GlobPattern is seperated into `Prefix + SubGlobPattern`
|
||||
|
||||
@@ -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,8 +18,6 @@
|
||||
|
||||
namespace clice {
|
||||
|
||||
namespace et = eventide;
|
||||
|
||||
// DependencyGraph implementation
|
||||
|
||||
void DependencyGraph::add_module(llvm::StringRef module_name, std::uint32_t path_id) {
|
||||
@@ -253,12 +251,13 @@ 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.
|
||||
et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
PathPool& path_pool,
|
||||
DependencyGraph& graph,
|
||||
ScanReport& report,
|
||||
ScanCache* ext_cache,
|
||||
et::event_loop& 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) {
|
||||
auto start_time = std::chrono::steady_clock::now();
|
||||
|
||||
// Reuse context groups and configs from cache when available (warm runs).
|
||||
@@ -316,10 +315,10 @@ et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
if(!pending.empty()) {
|
||||
LOG_INFO("Warming toolchain cache: {} unique queries", pending.size());
|
||||
|
||||
std::vector<et::task<ToolchainResult, et::error>> tasks;
|
||||
std::vector<kota::task<ToolchainResult, kota::error>> tasks;
|
||||
tasks.reserve(pending.size());
|
||||
for(auto& query: pending) {
|
||||
tasks.push_back(et::queue(
|
||||
tasks.push_back(kota::queue(
|
||||
[q = std::move(query)]() -> ToolchainResult {
|
||||
ToolchainResult result;
|
||||
result.key = q.key;
|
||||
@@ -337,7 +336,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
loop));
|
||||
}
|
||||
|
||||
auto outcome = co_await et::when_all(std::move(tasks));
|
||||
auto outcome = co_await kota::when_all(std::move(tasks));
|
||||
if(outcome.has_value()) {
|
||||
cdb.inject_results(*outcome);
|
||||
} else {
|
||||
@@ -357,9 +356,19 @@ et::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});
|
||||
configs[config_id] = cdb.lookup_search_config(
|
||||
representative_path,
|
||||
{.query_toolchain = true, .remove = rule_remove, .append = rule_append});
|
||||
auto t1 = std::chrono::steady_clock::now();
|
||||
lookup_us += std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
|
||||
}
|
||||
@@ -390,7 +399,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
llvm::StringSet<> entries;
|
||||
};
|
||||
|
||||
std::vector<et::task<DirEntry, et::error>> pending_dir_tasks;
|
||||
std::vector<kota::task<DirEntry, kota::error>> pending_dir_tasks;
|
||||
|
||||
if(dir_cache.dirs.empty()) {
|
||||
llvm::StringSet<> unique_dirs;
|
||||
@@ -412,7 +421,7 @@ et::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(et::queue(
|
||||
pending_dir_tasks.push_back(kota::queue(
|
||||
[dir_path = std::move(dir_path)]() -> DirEntry {
|
||||
DirEntry result;
|
||||
result.dir_path = dir_path;
|
||||
@@ -463,7 +472,7 @@ et::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<et::task<FileScanResult, et::error>> prefetch_tasks;
|
||||
std::vector<kota::task<FileScanResult, kota::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.
|
||||
@@ -500,7 +509,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
|
||||
if(!prefetch_tasks.empty()) {
|
||||
// Waves 1+: await prefetched scan tasks from previous Phase 2.
|
||||
auto scan_outcome = co_await et::when_all(std::move(prefetch_tasks));
|
||||
auto scan_outcome = co_await kota::when_all(std::move(prefetch_tasks));
|
||||
prefetch_tasks.clear();
|
||||
if(scan_outcome.has_error()) {
|
||||
LOG_ERROR("Prefetch scan failed: {}", scan_outcome.error().message());
|
||||
@@ -514,7 +523,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
}
|
||||
} else {
|
||||
// Wave 0 (or warm run with all cache hits): create scan tasks now.
|
||||
std::vector<et::task<FileScanResult, et::error>> scan_tasks;
|
||||
std::vector<kota::task<FileScanResult, kota::error>> scan_tasks;
|
||||
scan_tasks.reserve(current_wave.size());
|
||||
for(auto& entry: current_wave) {
|
||||
auto pid = entry.path_id;
|
||||
@@ -525,8 +534,8 @@ et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
}
|
||||
auto path = path_pool.resolve(pid).data();
|
||||
scan_tasks.push_back(
|
||||
et::queue([path, pid, cid]() { return scan_file_worker(path, pid, cid); },
|
||||
loop));
|
||||
kota::queue([path, pid, cid]() { return scan_file_worker(path, pid, cid); },
|
||||
loop));
|
||||
}
|
||||
|
||||
// Optimization 1: await dir cache tasks concurrently with scan tasks.
|
||||
@@ -535,7 +544,7 @@ et::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 et::when_all(std::move(pending_dir_tasks));
|
||||
auto dir_outcome = co_await kota::when_all(std::move(pending_dir_tasks));
|
||||
pending_dir_tasks.clear();
|
||||
if(dir_outcome.has_value()) {
|
||||
for(auto& entry: *dir_outcome) {
|
||||
@@ -549,7 +558,7 @@ et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
}
|
||||
|
||||
if(!scan_tasks.empty()) {
|
||||
auto scan_outcome = co_await et::when_all(std::move(scan_tasks));
|
||||
auto scan_outcome = co_await kota::when_all(std::move(scan_tasks));
|
||||
if(scan_outcome.has_error()) {
|
||||
LOG_ERROR("Parallel scan failed: {}", scan_outcome.error().message());
|
||||
break;
|
||||
@@ -749,7 +758,7 @@ et::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(et::queue(
|
||||
prefetch_tasks.push_back(kota::queue(
|
||||
[inc_path, inc_path_id, cid = scan_result.config_id]() {
|
||||
return scan_file_worker(inc_path, inc_path_id, cid);
|
||||
},
|
||||
@@ -821,14 +830,15 @@ et::task<> scan_impl(CompilationDatabase& cdb,
|
||||
ScanReport scan_dependency_graph(CompilationDatabase& cdb,
|
||||
PathPool& path_pool,
|
||||
DependencyGraph& graph,
|
||||
ScanCache* cache) {
|
||||
ScanCache* cache,
|
||||
const RuleMatcher& rule_matcher) {
|
||||
ScanReport report;
|
||||
if(cdb.get_entries().empty()) {
|
||||
return report;
|
||||
}
|
||||
|
||||
et::event_loop loop;
|
||||
loop.schedule(scan_impl(cdb, path_pool, graph, report, cache, loop));
|
||||
kota::event_loop loop;
|
||||
loop.schedule(scan_impl(cdb, path_pool, graph, report, cache, loop, rule_matcher));
|
||||
loop.run();
|
||||
return report;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -253,6 +254,12 @@ 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.
|
||||
@@ -261,9 +268,14 @@ struct ScanCache {
|
||||
/// 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);
|
||||
ScanCache* cache = nullptr,
|
||||
const RuleMatcher& rule_matcher = {});
|
||||
|
||||
} // namespace clice
|
||||
|
||||
@@ -53,7 +53,8 @@ void Lexer::lex(Token& token) {
|
||||
}
|
||||
} else if(parse_pp_keyword) {
|
||||
parse_pp_keyword = false;
|
||||
parse_header_name = token.text(content) == "include";
|
||||
auto kw = token.text(content);
|
||||
parse_header_name = kw == "include" || kw == "include_next" || kw == "embed";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,4 +106,60 @@ 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
|
||||
|
||||
@@ -51,6 +51,15 @@ public:
|
||||
|
||||
Token advance_until(TokenKind kind);
|
||||
|
||||
/// Force the lexer into header-name mode so the next token is lexed
|
||||
/// via LexIncludeFilename (correctly handling both "..." and <...>).
|
||||
/// Use this before lexing filename arguments in contexts like
|
||||
/// __has_include() or __has_embed() where the lexer cannot detect
|
||||
/// the mode automatically.
|
||||
void set_header_name_mode() {
|
||||
parse_header_name = true;
|
||||
}
|
||||
|
||||
private:
|
||||
bool ignore_end_of_directive = true;
|
||||
bool parse_pp_keyword = false;
|
||||
@@ -64,4 +73,13 @@ 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
|
||||
|
||||
@@ -109,7 +109,13 @@ async def client(
|
||||
await c.start_io(*cmd)
|
||||
|
||||
if workspace is not None:
|
||||
await c.initialize(workspace)
|
||||
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)
|
||||
|
||||
yield c
|
||||
|
||||
@@ -163,12 +169,17 @@ async def _shutdown_client(c: CliceClient) -> None:
|
||||
|
||||
try:
|
||||
server = getattr(c, "_server", None)
|
||||
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)
|
||||
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)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -231,6 +242,23 @@ def _generate_test_data_cdbs(data_dir: Path) -> None:
|
||||
if ic_main.exists():
|
||||
_write(ic_dir, [_entry(ic_dir, ic_main, ["-I."])])
|
||||
|
||||
# document_links
|
||||
dl_dir = data_dir / "document_links"
|
||||
dl_main = dl_dir / "main.cpp"
|
||||
if dl_main.exists():
|
||||
_write(
|
||||
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():
|
||||
|
||||
7
tests/data/config_rules_no_config/main.cpp
Normal file
7
tests/data/config_rules_no_config/main.cpp
Normal file
@@ -0,0 +1,7 @@
|
||||
int value() {
|
||||
return FROM_INIT;
|
||||
}
|
||||
|
||||
int main() {
|
||||
return value();
|
||||
}
|
||||
3
tests/data/config_rules_toml/clice.toml
Normal file
3
tests/data/config_rules_toml/clice.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[[rules]]
|
||||
patterns = ["**/*.cpp"]
|
||||
append = ["-DFROM_TOML"]
|
||||
7
tests/data/config_rules_toml/main.cpp
Normal file
7
tests/data/config_rules_toml/main.cpp
Normal file
@@ -0,0 +1,7 @@
|
||||
int value() {
|
||||
return FROM_TOML;
|
||||
}
|
||||
|
||||
int main() {
|
||||
return value();
|
||||
}
|
||||
1
tests/data/document_links/data.bin
Normal file
1
tests/data/document_links/data.bin
Normal file
@@ -0,0 +1 @@
|
||||
0123456789
|
||||
3
tests/data/document_links/header_a.h
Normal file
3
tests/data/document_links/header_a.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
int a = 1;
|
||||
3
tests/data/document_links/header_b.h
Normal file
3
tests/data/document_links/header_b.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
int b = 2;
|
||||
3
tests/data/document_links/header_c.h
Normal file
3
tests/data/document_links/header_c.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
int c = 3;
|
||||
20
tests/data/document_links/main.cpp
Normal file
20
tests/data/document_links/main.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#include "header_a.h"
|
||||
#include "header_b.h"
|
||||
int x = 1;
|
||||
#include "header_c.h"
|
||||
|
||||
const char data[] = {
|
||||
#embed "data.bin"
|
||||
};
|
||||
|
||||
#if __has_embed("data.bin")
|
||||
int has_embed_found = 1;
|
||||
#endif
|
||||
|
||||
#if __has_embed("no_such_file.bin")
|
||||
int has_embed_not_found = 1;
|
||||
#endif
|
||||
|
||||
int main() {
|
||||
return a + b + c;
|
||||
}
|
||||
@@ -24,9 +24,17 @@ 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'
|
||||
@@ -48,6 +56,7 @@ 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'
|
||||
@@ -74,6 +83,7 @@ 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'
|
||||
@@ -108,6 +118,7 @@ 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'
|
||||
@@ -150,6 +161,7 @@ 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'
|
||||
@@ -176,6 +188,7 @@ 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')
|
||||
@@ -199,6 +212,7 @@ 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'
|
||||
@@ -240,6 +254,7 @@ 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'
|
||||
@@ -265,6 +280,7 @@ 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)
|
||||
|
||||
103
tests/integration/features/test_document_links.py
Normal file
103
tests/integration/features/test_document_links.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.workspace("document_links")
|
||||
async def test_document_links_with_pch(client, workspace):
|
||||
uri, content = await client.open_and_wait(workspace / "main.cpp")
|
||||
links = await client.document_links(uri)
|
||||
|
||||
assert links is not None, "document_links returned None"
|
||||
|
||||
targets = sorted(Path(link.target).name for link in links)
|
||||
assert targets == [
|
||||
"data.bin",
|
||||
"data.bin",
|
||||
"header_a.h",
|
||||
"header_b.h",
|
||||
"header_c.h",
|
||||
], f"Unexpected targets: {targets}"
|
||||
|
||||
client.close(uri)
|
||||
|
||||
|
||||
@pytest.mark.workspace("document_links")
|
||||
async def test_document_links_pch_portion(client, workspace):
|
||||
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
links = await client.document_links(uri)
|
||||
|
||||
pch_links = [link for link in links if link.range.start.line < 2]
|
||||
assert len(pch_links) == 2, (
|
||||
f"Expected 2 PCH links (lines 0-1), got {len(pch_links)}"
|
||||
)
|
||||
|
||||
pch_targets = sorted(Path(link.target).name for link in pch_links)
|
||||
assert pch_targets == ["header_a.h", "header_b.h"]
|
||||
|
||||
client.close(uri)
|
||||
|
||||
|
||||
@pytest.mark.workspace("document_links")
|
||||
async def test_document_links_main_portion(client, workspace):
|
||||
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
links = await client.document_links(uri)
|
||||
|
||||
main_links = [link for link in links if link.range.start.line >= 2]
|
||||
assert len(main_links) == 3, (
|
||||
f"Expected 3 main-file links (lines 3, 6, 9), got {len(main_links)}"
|
||||
)
|
||||
|
||||
main_targets = sorted(Path(link.target).name for link in main_links)
|
||||
assert main_targets == ["data.bin", "data.bin", "header_c.h"]
|
||||
|
||||
client.close(uri)
|
||||
|
||||
|
||||
@pytest.mark.workspace("document_links")
|
||||
async def test_document_links_embed(client, workspace):
|
||||
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
links = await client.document_links(uri)
|
||||
|
||||
embed_links = [
|
||||
link
|
||||
for link in links
|
||||
if Path(link.target).name == "data.bin" and link.range.start.line == 6
|
||||
]
|
||||
assert len(embed_links) == 1, (
|
||||
f"Expected 1 embed link at line 6, got {len(embed_links)}"
|
||||
)
|
||||
|
||||
client.close(uri)
|
||||
|
||||
|
||||
@pytest.mark.workspace("document_links")
|
||||
async def test_document_links_has_embed_exists(client, workspace):
|
||||
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
links = await client.document_links(uri)
|
||||
|
||||
has_embed_links = [
|
||||
link
|
||||
for link in links
|
||||
if Path(link.target).name == "data.bin" and link.range.start.line == 9
|
||||
]
|
||||
assert len(has_embed_links) == 1, (
|
||||
f"Expected 1 has_embed link at line 9, got {len(has_embed_links)}"
|
||||
)
|
||||
|
||||
client.close(uri)
|
||||
|
||||
|
||||
@pytest.mark.workspace("document_links")
|
||||
async def test_document_links_has_embed_missing(client, workspace):
|
||||
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
||||
links = await client.document_links(uri)
|
||||
|
||||
missing_links = [
|
||||
link for link in links if Path(link.target).name == "no_such_file.bin"
|
||||
]
|
||||
assert len(missing_links) == 0, (
|
||||
f"Expected 0 links for non-existent file, got {len(missing_links)}"
|
||||
)
|
||||
|
||||
client.close(uri)
|
||||
68
tests/integration/lifecycle/test_config.py
Normal file
68
tests/integration/lifecycle/test_config.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""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")
|
||||
@@ -86,16 +86,20 @@ class CliceClient(BaseLanguageClient):
|
||||
|
||||
# ── Lifecycle ────────────────────────────────────────────────────
|
||||
|
||||
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")
|
||||
],
|
||||
)
|
||||
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")],
|
||||
)
|
||||
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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
[pytest]
|
||||
asyncio_mode = auto
|
||||
markers = workspace
|
||||
markers =
|
||||
workspace
|
||||
init_options
|
||||
|
||||
@@ -13,6 +13,9 @@ 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
|
||||
|
||||
@@ -109,7 +112,9 @@ 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) -> bool | None:
|
||||
async def replay_one(
|
||||
trace_path: Path, clice_bin: Path, timeout: int, wall_timeout: int = 300
|
||||
) -> bool | None:
|
||||
"""Replay a single trace. Returns True=PASS, False=FAIL, None=SKIP."""
|
||||
records = load_trace(trace_path)
|
||||
if not records:
|
||||
@@ -179,8 +184,21 @@ async def replay_one(trace_path: Path, clice_bin: Path, timeout: int) -> bool |
|
||||
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:
|
||||
@@ -196,7 +214,7 @@ async def replay_one(trace_path: Path, clice_bin: Path, timeout: int) -> bool |
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
asyncio.gather(*pending.values(), return_exceptions=True),
|
||||
timeout=timeout,
|
||||
timeout=min(timeout, remaining_wall()),
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
elapsed = time.monotonic() - wall_start
|
||||
@@ -210,7 +228,19 @@ async def replay_one(trace_path: Path, clice_bin: Path, timeout: int) -> bool |
|
||||
if msg_id is not None and method is not None:
|
||||
pending[msg_id] = asyncio.get_event_loop().create_future()
|
||||
|
||||
await write_lsp_message(proc.stdin, rec["msg"])
|
||||
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
|
||||
sent_count = i + 1
|
||||
|
||||
except (ConnectionError, BrokenPipeError):
|
||||
@@ -231,7 +261,7 @@ async def replay_one(trace_path: Path, clice_bin: Path, timeout: int) -> bool |
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
asyncio.gather(*pending.values(), return_exceptions=True),
|
||||
timeout=timeout,
|
||||
timeout=min(timeout, remaining_wall()),
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
elapsed = time.monotonic() - wall_start
|
||||
@@ -294,7 +324,7 @@ async def async_main(args):
|
||||
print(f"SKIP: {trace} (not found)")
|
||||
skipped += 1
|
||||
continue
|
||||
result = await replay_one(trace, args.clice, args.timeout)
|
||||
result = await replay_one(trace, args.clice, args.timeout, args.wall_timeout)
|
||||
if result is None:
|
||||
skipped += 1
|
||||
elif result:
|
||||
@@ -317,7 +347,16 @@ 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="Timeout in seconds (default: 120)"
|
||||
"--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)",
|
||||
)
|
||||
args = p.parse_args()
|
||||
sys.exit(asyncio.run(async_main(args)))
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace clice::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace protocol = eventide::ipc::protocol;
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
|
||||
TEST_SUITE(CodeCompletion) {
|
||||
|
||||
@@ -233,6 +233,33 @@ 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;
|
||||
|
||||
@@ -9,15 +9,15 @@ namespace clice::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace protocol = eventide::ipc::protocol;
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
|
||||
TEST_SUITE(DocumentLink, Tester) {
|
||||
|
||||
std::vector<protocol::DocumentLink> links;
|
||||
|
||||
void run(llvm::StringRef source) {
|
||||
void run(llvm::StringRef source, llvm::StringRef standard = "-std=c++17") {
|
||||
add_files("main.cpp", source);
|
||||
ASSERT_TRUE(compile());
|
||||
ASSERT_TRUE(compile(standard));
|
||||
links = feature::document_links(*unit, feature::PositionEncoding::UTF8);
|
||||
}
|
||||
|
||||
@@ -89,6 +89,53 @@ TEST_CASE(HasInclude) {
|
||||
EXPECT_LINK(1, "1", TestVFS::path("test.h"));
|
||||
}
|
||||
|
||||
TEST_CASE(MacroInclude) {
|
||||
run(R"cpp(
|
||||
#[test.h]
|
||||
|
||||
#[main.cpp]
|
||||
#define HEADER "test.h"
|
||||
#include @0[HEADER$]
|
||||
)cpp");
|
||||
|
||||
ASSERT_EQ(links.size(), 1U);
|
||||
EXPECT_LINK(0, "0", TestVFS::path("test.h"));
|
||||
}
|
||||
|
||||
TEST_CASE(Embed) {
|
||||
run(R"cpp(
|
||||
#[bytes.bin]
|
||||
0123456789
|
||||
|
||||
#[main.cpp]
|
||||
const char e[] = {
|
||||
#embed @0["bytes.bin"$]
|
||||
};
|
||||
)cpp",
|
||||
"-std=c++23");
|
||||
|
||||
ASSERT_EQ(links.size(), 1U);
|
||||
EXPECT_LINK(0, "0", TestVFS::path("bytes.bin"));
|
||||
}
|
||||
|
||||
TEST_CASE(HasEmbed) {
|
||||
run(R"cpp(
|
||||
#[data.bin]
|
||||
ABCDE
|
||||
|
||||
#[main.cpp]
|
||||
#if __has_embed(@0["data.bin"$])
|
||||
#endif
|
||||
|
||||
#if __has_embed("non_existent.bin")
|
||||
#endif
|
||||
)cpp",
|
||||
"-std=c++23");
|
||||
|
||||
ASSERT_EQ(links.size(), 1U);
|
||||
EXPECT_LINK(0, "0", TestVFS::path("data.bin"));
|
||||
}
|
||||
|
||||
}; // TEST_SUITE(DocumentLink)
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace clice::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace protocol = eventide::ipc::protocol;
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
|
||||
TEST_SUITE(DocumentSymbol, Tester) {
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace clice::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace protocol = eventide::ipc::protocol;
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
|
||||
TEST_SUITE(FoldingRange, Tester) {
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace clice::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace protocol = eventide::ipc::protocol;
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
|
||||
TEST_SUITE(Hover, Tester) {
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace clice::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace protocol = eventide::ipc::protocol;
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
|
||||
TEST_SUITE(InlayHint, Tester) {
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace clice::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace protocol = eventide::ipc::protocol;
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
|
||||
struct DecodedToken {
|
||||
LocalSourceRange range;
|
||||
@@ -423,6 +423,122 @@ 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
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace clice::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace protocol = eventide::ipc::protocol;
|
||||
namespace protocol = kota::ipc::protocol;
|
||||
|
||||
TEST_SUITE(SignatureHelp, Tester) {
|
||||
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
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.
|
||||
@@ -20,7 +18,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) -> et::task<bool> {
|
||||
return [&](std::uint32_t path_id) -> kota::task<bool> {
|
||||
auto file_path = pool.resolve(path_id);
|
||||
auto results = cdb.lookup(file_path, {.query_toolchain = true, .suppress_logging = true});
|
||||
if(results.empty()) {
|
||||
@@ -123,8 +121,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));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_a]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_a]() -> kota::task<> {
|
||||
auto result = co_await cg.compile(pid_a).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -157,8 +155,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));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_a, pid_b]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_a, pid_b]() -> kota::task<> {
|
||||
auto result = co_await cg.compile(pid_b).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -202,8 +200,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));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_top]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_top]() -> kota::task<> {
|
||||
auto result = co_await cg.compile(pid_top).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -238,8 +236,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));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_app]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_app]() -> kota::task<> {
|
||||
auto result = co_await cg.compile(pid_app).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -280,8 +278,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));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_user]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_user]() -> kota::task<> {
|
||||
auto result = co_await cg.compile(pid_user).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -324,8 +322,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));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid]() -> kota::task<> {
|
||||
auto result = co_await cg.compile(pid).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -360,8 +358,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));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid]() -> kota::task<> {
|
||||
auto result = co_await cg.compile(pid).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -396,8 +394,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));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid]() -> kota::task<> {
|
||||
auto result = co_await cg.compile(pid).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -436,8 +434,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));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_m]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_m]() -> kota::task<> {
|
||||
auto result = co_await cg.compile(pid_m).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -476,8 +474,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));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_lib]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_lib]() -> kota::task<> {
|
||||
auto result = co_await cg.compile(pid_lib).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -519,8 +517,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));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_sys]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_sys]() -> kota::task<> {
|
||||
auto result = co_await cg.compile(pid_sys).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -561,8 +559,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));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid]() -> kota::task<> {
|
||||
auto result = co_await cg.compile(pid).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -600,8 +598,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));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid]() -> kota::task<> {
|
||||
auto result = co_await cg.compile(pid).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -651,8 +649,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));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid]() -> kota::task<> {
|
||||
auto result = co_await cg.compile(pid).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -686,8 +684,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));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_x, pid_y]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_x, pid_y]() -> kota::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();
|
||||
@@ -728,8 +726,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));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid]() -> kota::task<> {
|
||||
auto result = co_await cg.compile(pid).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -775,8 +773,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));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid]() -> kota::task<> {
|
||||
auto result = co_await cg.compile(pid).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -813,8 +811,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));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_leaf, pid_mid]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_leaf, pid_mid]() -> kota::task<> {
|
||||
// First compile.
|
||||
auto r1 = co_await cg.compile(pid_mid).catch_cancel();
|
||||
EXPECT_TRUE(r1.has_value() && *r1);
|
||||
@@ -864,8 +862,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));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid]() -> kota::task<> {
|
||||
auto result = co_await cg.compile(pid).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -905,8 +903,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));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_app]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_app]() -> kota::task<> {
|
||||
auto result = co_await cg.compile(pid_app).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -958,8 +956,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));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_base, pid_left, pid_right, pid_top]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_base, pid_left, pid_right, pid_top]() -> kota::task<> {
|
||||
// Initial compile.
|
||||
auto r1 = co_await cg.compile(pid_top).catch_cancel();
|
||||
EXPECT_TRUE(r1.has_value() && *r1);
|
||||
@@ -1046,8 +1044,8 @@ TEST_CASE(ReResolveAfterUpdate) {
|
||||
CompileGraph cg(make_dispatch(env.cdb, env.pool, env.graph, env.pcm_paths),
|
||||
std::move(counting_resolver));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, &resolve_count, pid_mid]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, &resolve_count, pid_mid]() -> kota::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);
|
||||
@@ -1092,8 +1090,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));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_bad]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_bad]() -> kota::task<> {
|
||||
auto result = co_await cg.compile(pid_bad).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
// Compilation should fail due to undefined symbol.
|
||||
@@ -1133,8 +1131,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));
|
||||
|
||||
et::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_iface]() -> et::task<> {
|
||||
kota::event_loop loop;
|
||||
auto test = [this, &cg, &env, pid_iface]() -> kota::task<> {
|
||||
// Build the interface PCM via CompileGraph.
|
||||
auto r1 = co_await cg.compile(pid_iface).catch_cancel();
|
||||
EXPECT_TRUE(r1.has_value() && *r1);
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
namespace clice::testing {
|
||||
namespace {
|
||||
|
||||
namespace et = eventide;
|
||||
namespace ranges = std::ranges;
|
||||
|
||||
/// A resolve_fn that always returns no dependencies.
|
||||
@@ -29,27 +28,27 @@ CompileGraph::resolve_fn
|
||||
}
|
||||
|
||||
CompileGraph::dispatch_fn instant_dispatch() {
|
||||
return [](std::uint32_t) -> et::task<bool> {
|
||||
return [](std::uint32_t) -> kota::task<bool> {
|
||||
co_return true;
|
||||
};
|
||||
}
|
||||
|
||||
CompileGraph::dispatch_fn tracking_dispatch(std::vector<std::uint32_t>& compiled) {
|
||||
return [&compiled](std::uint32_t path_id) -> et::task<bool> {
|
||||
return [&compiled](std::uint32_t path_id) -> kota::task<bool> {
|
||||
compiled.push_back(path_id);
|
||||
co_return true;
|
||||
};
|
||||
}
|
||||
|
||||
CompileGraph::dispatch_fn failing_dispatch() {
|
||||
return [](std::uint32_t) -> et::task<bool> {
|
||||
return [](std::uint32_t) -> kota::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) -> et::task<bool> {
|
||||
return [fail_ids = std::move(fail_ids)](std::uint32_t path_id) -> kota::task<bool> {
|
||||
co_return !fail_ids.contains(path_id);
|
||||
};
|
||||
}
|
||||
@@ -61,7 +60,7 @@ std::optional<CompileGraph> graph;
|
||||
|
||||
template <typename F>
|
||||
void execute(F&& fn) {
|
||||
et::event_loop loop;
|
||||
kota::event_loop loop;
|
||||
auto t = fn();
|
||||
loop.schedule(t);
|
||||
loop.run();
|
||||
@@ -70,7 +69,7 @@ void execute(F&& fn) {
|
||||
TEST_CASE(CompileNoDeps) {
|
||||
graph.emplace(tracking_dispatch(compiled), no_deps());
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
auto result = co_await graph->compile(1).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -87,7 +86,7 @@ TEST_CASE(CompileWithDependency) {
|
||||
{1, {2}}
|
||||
}));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
auto result = co_await graph->compile(1).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -109,7 +108,7 @@ TEST_CASE(CompileChain) {
|
||||
{2, {3}}
|
||||
}));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
auto result = co_await graph->compile(1).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -132,7 +131,7 @@ TEST_CASE(DiamondDependency) {
|
||||
{3, {4} }
|
||||
}));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
auto result = co_await graph->compile(1).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -152,7 +151,7 @@ TEST_CASE(UpdateInvalidates) {
|
||||
{1, {2}}
|
||||
}));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
co_await graph->compile(1).catch_cancel();
|
||||
EXPECT_FALSE(graph->is_dirty(2));
|
||||
EXPECT_FALSE(graph->is_dirty(1));
|
||||
@@ -172,7 +171,7 @@ TEST_CASE(UpdateCascade) {
|
||||
{2, {3}}
|
||||
}));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
co_await graph->compile(1).catch_cancel();
|
||||
EXPECT_FALSE(graph->is_dirty(2));
|
||||
EXPECT_FALSE(graph->is_dirty(3));
|
||||
@@ -192,7 +191,7 @@ TEST_CASE(CompileAfterUpdate) {
|
||||
{1, {2}}
|
||||
}));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
co_await graph->compile(1).catch_cancel();
|
||||
EXPECT_EQ(compiled.size(), 2u);
|
||||
|
||||
@@ -210,7 +209,7 @@ TEST_CASE(DispatchFailure) {
|
||||
{1, {2}}
|
||||
}));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
auto result = co_await graph->compile(1).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_FALSE(*result);
|
||||
@@ -228,7 +227,7 @@ TEST_CASE(CancelAll) {
|
||||
TEST_CASE(SecondCompileSkips) {
|
||||
graph.emplace(tracking_dispatch(compiled), no_deps());
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
co_await graph->compile(1).catch_cancel();
|
||||
EXPECT_EQ(compiled.size(), 1u);
|
||||
// Second compile should skip (already clean).
|
||||
@@ -245,7 +244,7 @@ TEST_CASE(CascadeThroughAlreadyDirty) {
|
||||
{2, {3}}
|
||||
}));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
co_await graph->compile(1).catch_cancel();
|
||||
|
||||
// Update node 2: marks 2 and 1 dirty.
|
||||
@@ -270,7 +269,7 @@ TEST_CASE(CircularDependencyDetection) {
|
||||
{2, {1}}
|
||||
}));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
auto result = co_await graph->compile(1).catch_cancel();
|
||||
// Should return false (cycle detected), not deadlock.
|
||||
EXPECT_TRUE(result.has_value());
|
||||
@@ -289,7 +288,7 @@ TEST_CASE(CrossBranchCycleDetection) {
|
||||
{3, {2} }
|
||||
}));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
auto result = co_await graph->compile(1).catch_cancel();
|
||||
// Should return false (cycle detected), not deadlock.
|
||||
EXPECT_TRUE(result.has_value());
|
||||
@@ -312,7 +311,7 @@ TEST_CASE(UpdateResetsResolved) {
|
||||
|
||||
graph.emplace(tracking_dispatch(compiled), std::move(resolver));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
// First compile: resolves 1 -> {2}.
|
||||
co_await graph->compile(1).catch_cancel();
|
||||
EXPECT_EQ(resolve_count, 1);
|
||||
@@ -344,7 +343,7 @@ TEST_CASE(UpdateCleansBackEdges) {
|
||||
|
||||
graph.emplace(tracking_dispatch(compiled), std::move(resolver));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
// First compile: 1 -> {2}.
|
||||
co_await graph->compile(1).catch_cancel();
|
||||
EXPECT_FALSE(graph->is_dirty(1));
|
||||
@@ -373,7 +372,7 @@ TEST_CASE(DiamondUpdateCascade) {
|
||||
{3, {4} }
|
||||
}));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
co_await graph->compile(1).catch_cancel();
|
||||
EXPECT_FALSE(graph->is_dirty(1));
|
||||
EXPECT_FALSE(graph->is_dirty(4));
|
||||
@@ -402,7 +401,7 @@ TEST_CASE(UpdateReturnsAllDirtied) {
|
||||
{2, {3}}
|
||||
}));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
co_await graph->compile(1).catch_cancel();
|
||||
|
||||
auto dirtied = graph->update(3);
|
||||
@@ -417,7 +416,7 @@ TEST_CASE(UpdateReturnsAllDirtied) {
|
||||
TEST_CASE(HasUnitAndIsCompiling) {
|
||||
graph.emplace(instant_dispatch(), no_deps());
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
EXPECT_FALSE(graph->has_unit(1));
|
||||
EXPECT_FALSE(graph->is_compiling(1));
|
||||
|
||||
@@ -434,7 +433,7 @@ TEST_CASE(FailureLeavesDepsDirty) {
|
||||
{1, {2}}
|
||||
}));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
auto result = co_await graph->compile(1).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_FALSE(*result);
|
||||
@@ -451,7 +450,7 @@ TEST_CASE(SelfLoop) {
|
||||
{1, {1}}
|
||||
}));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
auto result = co_await graph->compile(1).catch_cancel();
|
||||
// Should detect cycle and return false, not deadlock.
|
||||
EXPECT_TRUE(result.has_value());
|
||||
@@ -465,7 +464,7 @@ TEST_CASE(CancelAllAndRecompile) {
|
||||
{1, {2}}
|
||||
}));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
co_await graph->compile(1).catch_cancel();
|
||||
EXPECT_EQ(compiled.size(), 2u);
|
||||
EXPECT_FALSE(graph->is_dirty(1));
|
||||
@@ -488,10 +487,10 @@ TEST_CASE(CancelAllAndRecompile) {
|
||||
}
|
||||
|
||||
TEST_CASE(UpdateDuringCompile) {
|
||||
et::event_loop loop;
|
||||
et::event gate;
|
||||
kota::event_loop loop;
|
||||
kota::event gate;
|
||||
|
||||
auto gated_dispatch = [&gate](std::uint32_t) -> et::task<bool> {
|
||||
auto gated_dispatch = [&gate](std::uint32_t) -> kota::task<bool> {
|
||||
co_await gate.wait();
|
||||
co_return true;
|
||||
};
|
||||
@@ -502,14 +501,14 @@ TEST_CASE(UpdateDuringCompile) {
|
||||
bool was_cancelled = false;
|
||||
|
||||
// Coroutine 1: compile(1), will suspend inside dispatch waiting on gate.
|
||||
auto compiler = [&]() -> et::task<> {
|
||||
auto compiler = [&]() -> kota::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 = [&]() -> et::task<> {
|
||||
auto updater = [&]() -> kota::task<> {
|
||||
graph->update(1);
|
||||
gate.set();
|
||||
co_return;
|
||||
@@ -534,7 +533,7 @@ TEST_CASE(WhenAllPartialFailure) {
|
||||
}),
|
||||
static_resolver({{1, {2, 3}}}));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
auto result = co_await graph->compile(1).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_FALSE(*result);
|
||||
@@ -566,7 +565,7 @@ TEST_CASE(EmptyGraphNoCompile) {
|
||||
TEST_CASE(CompileDepsNoDeps) {
|
||||
graph.emplace(tracking_dispatch(compiled), no_deps());
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
auto result = co_await graph->compile_deps(1).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -582,7 +581,7 @@ TEST_CASE(CompileDepsWithDependency) {
|
||||
{1, {2}}
|
||||
}));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
auto result = co_await graph->compile_deps(1).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -602,7 +601,7 @@ TEST_CASE(CompileDepsChain) {
|
||||
{2, {3}}
|
||||
}));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
auto result = co_await graph->compile_deps(1).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -623,7 +622,7 @@ TEST_CASE(CompileDepsDiamond) {
|
||||
{3, {4} }
|
||||
}));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
auto result = co_await graph->compile_deps(1).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -640,7 +639,7 @@ TEST_CASE(CompileDepsDiamond) {
|
||||
|
||||
TEST_CASE(CompileDepsFailure) {
|
||||
// 1 -> 2. Dispatch fails for unit 2.
|
||||
auto fail_and_track = [&](std::uint32_t path_id) -> et::task<bool> {
|
||||
auto fail_and_track = [&](std::uint32_t path_id) -> kota::task<bool> {
|
||||
compiled.push_back(path_id);
|
||||
co_return false;
|
||||
};
|
||||
@@ -650,7 +649,7 @@ TEST_CASE(CompileDepsFailure) {
|
||||
{1, {2}}
|
||||
}));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
auto result = co_await graph->compile_deps(1).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_FALSE(*result);
|
||||
@@ -666,7 +665,7 @@ TEST_CASE(CompileDepsPlainCpp) {
|
||||
{10, {20}}
|
||||
}));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
auto result = co_await graph->compile_deps(10).catch_cancel();
|
||||
EXPECT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(*result);
|
||||
@@ -688,11 +687,11 @@ TEST_CASE(CompileDepsConcurrentDedup) {
|
||||
{2, {3, 5}},
|
||||
}));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
// Launch both compile_deps concurrently.
|
||||
auto t1 = graph->compile_deps(1);
|
||||
auto t2 = graph->compile_deps(2);
|
||||
auto results = co_await et::when_all(std::move(t1), std::move(t2));
|
||||
auto results = co_await kota::when_all(std::move(t1), std::move(t2));
|
||||
|
||||
auto [r1, r2] = results;
|
||||
EXPECT_TRUE(r1);
|
||||
@@ -722,10 +721,10 @@ TEST_CASE(CompileDepsResolveOnce) {
|
||||
|
||||
graph.emplace(tracking_dispatch(compiled), std::move(resolve));
|
||||
|
||||
execute([&]() -> et::task<> {
|
||||
execute([&]() -> kota::task<> {
|
||||
auto t1 = graph->compile_deps(1);
|
||||
auto t2 = graph->compile_deps(2);
|
||||
auto results = co_await et::when_all(std::move(t1), std::move(t2));
|
||||
auto results = co_await kota::when_all(std::move(t1), std::move(t2));
|
||||
|
||||
auto [r1, r2] = results;
|
||||
EXPECT_TRUE(r1);
|
||||
|
||||
501
tests/unit/server/config_tests.cpp
Normal file
501
tests/unit/server/config_tests.cpp
Normal file
@@ -0,0 +1,501 @@
|
||||
#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
|
||||
@@ -9,8 +9,6 @@ namespace clice::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace et = eventide;
|
||||
|
||||
// ============================================================================
|
||||
// End-to-end module compilation through real workers:
|
||||
// 1. Stateless worker builds PCM for module interface
|
||||
@@ -38,7 +36,7 @@ TEST_CASE(BuildPCMThenCompileWithImport) {
|
||||
std::string pcm_path;
|
||||
bool phase1_done = false;
|
||||
|
||||
sl.run([&]() -> et::task<> {
|
||||
sl.run([&]() -> kota::task<> {
|
||||
worker::BuildParams params;
|
||||
params.kind = worker::BuildKind::BuildPCM;
|
||||
params.file = iface;
|
||||
@@ -71,7 +69,7 @@ TEST_CASE(BuildPCMThenCompileWithImport) {
|
||||
|
||||
bool phase2_done = false;
|
||||
|
||||
sf.run([&]() -> et::task<> {
|
||||
sf.run([&]() -> kota::task<> {
|
||||
worker::CompileParams params;
|
||||
params.path = consumer;
|
||||
params.version = 1;
|
||||
@@ -123,7 +121,7 @@ TEST_CASE(BuildPCMChainThenCompile) {
|
||||
std::string pcm_a, pcm_b;
|
||||
bool pcm_done = false;
|
||||
|
||||
sl.run([&]() -> et::task<> {
|
||||
sl.run([&]() -> kota::task<> {
|
||||
// Build PCM for A first.
|
||||
{
|
||||
worker::BuildParams params;
|
||||
@@ -179,7 +177,7 @@ TEST_CASE(BuildPCMChainThenCompile) {
|
||||
|
||||
bool compile_done = false;
|
||||
|
||||
sf.run([&]() -> et::task<> {
|
||||
sf.run([&]() -> kota::task<> {
|
||||
worker::CompileParams params;
|
||||
params.path = consumer;
|
||||
params.version = 1;
|
||||
@@ -227,7 +225,7 @@ TEST_CASE(ModuleImplementationUnitWithWorker) {
|
||||
std::string pcm_path;
|
||||
bool pcm_done = false;
|
||||
|
||||
sl.run([&]() -> et::task<> {
|
||||
sl.run([&]() -> kota::task<> {
|
||||
worker::BuildParams params;
|
||||
params.kind = worker::BuildKind::BuildPCM;
|
||||
params.file = iface;
|
||||
@@ -257,7 +255,7 @@ TEST_CASE(ModuleImplementationUnitWithWorker) {
|
||||
|
||||
bool compile_done = false;
|
||||
|
||||
sf.run([&]() -> et::task<> {
|
||||
sf.run([&]() -> kota::task<> {
|
||||
worker::CompileParams params;
|
||||
params.path = impl;
|
||||
params.version = 1;
|
||||
|
||||
@@ -10,8 +10,6 @@ namespace clice::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace et = eventide;
|
||||
|
||||
// ============================================================================
|
||||
// End-to-end PCH compilation through real workers:
|
||||
// 1. Stateless worker builds PCH for preamble headers
|
||||
@@ -39,7 +37,7 @@ TEST_CASE(BuildPCHThenCompile) {
|
||||
std::string pch_path;
|
||||
bool phase1_done = false;
|
||||
|
||||
sl.run([&]() -> et::task<> {
|
||||
sl.run([&]() -> kota::task<> {
|
||||
worker::BuildParams params;
|
||||
params.kind = worker::BuildKind::BuildPCH;
|
||||
params.file = main_file;
|
||||
@@ -79,7 +77,7 @@ TEST_CASE(BuildPCHThenCompile) {
|
||||
|
||||
auto preamble_bound = compute_preamble_bound(main_text);
|
||||
|
||||
sf.run([&]() -> et::task<> {
|
||||
sf.run([&]() -> kota::task<> {
|
||||
worker::CompileParams params;
|
||||
params.path = main_file;
|
||||
params.version = 1;
|
||||
@@ -123,7 +121,7 @@ TEST_CASE(CompileWithoutPCHStillWorks) {
|
||||
|
||||
bool compile_done = false;
|
||||
|
||||
sf.run([&]() -> et::task<> {
|
||||
sf.run([&]() -> kota::task<> {
|
||||
worker::CompileParams params;
|
||||
params.path = main_file;
|
||||
params.version = 1;
|
||||
|
||||
@@ -2,16 +2,15 @@
|
||||
#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) {
|
||||
@@ -33,7 +32,7 @@ TEST_CASE(CompileRequest) {
|
||||
|
||||
bool test_done = false;
|
||||
|
||||
w.run([&]() -> et::task<> {
|
||||
w.run([&]() -> kota::task<> {
|
||||
worker::CompileParams params;
|
||||
params.path = src;
|
||||
params.version = 1;
|
||||
@@ -59,7 +58,7 @@ TEST_CASE(HoverWithoutCompile) {
|
||||
|
||||
bool test_done = false;
|
||||
|
||||
w.run([&]() -> et::task<> {
|
||||
w.run([&]() -> kota::task<> {
|
||||
// Hover on a file that hasn't been compiled should return null.
|
||||
worker::QueryParams params;
|
||||
params.kind = worker::QueryKind::Hover;
|
||||
@@ -88,7 +87,7 @@ TEST_CASE(CompileThenHover) {
|
||||
|
||||
bool test_done = false;
|
||||
|
||||
w.run([&]() -> et::task<> {
|
||||
w.run([&]() -> kota::task<> {
|
||||
// First compile
|
||||
worker::CompileParams cp;
|
||||
cp.path = src;
|
||||
@@ -129,7 +128,7 @@ TEST_CASE(DocumentUpdate) {
|
||||
|
||||
bool test_done = false;
|
||||
|
||||
w.run([&]() -> et::task<> {
|
||||
w.run([&]() -> kota::task<> {
|
||||
// Compile first
|
||||
worker::CompileParams cp;
|
||||
cp.path = src;
|
||||
@@ -170,7 +169,7 @@ TEST_CASE(CodeActionReturnsEmpty) {
|
||||
|
||||
bool test_done = false;
|
||||
|
||||
w.run([&]() -> et::task<> {
|
||||
w.run([&]() -> kota::task<> {
|
||||
worker::QueryParams params;
|
||||
params.kind = worker::QueryKind::CodeAction;
|
||||
params.path = "/tmp/test.cpp";
|
||||
@@ -192,7 +191,7 @@ TEST_CASE(GoToDefinitionReturnsEmpty) {
|
||||
|
||||
bool test_done = false;
|
||||
|
||||
w.run([&]() -> et::task<> {
|
||||
w.run([&]() -> kota::task<> {
|
||||
worker::QueryParams params;
|
||||
params.kind = worker::QueryKind::GoToDefinition;
|
||||
params.path = "/tmp/test.cpp";
|
||||
@@ -215,7 +214,7 @@ TEST_CASE(SemanticTokensWithoutCompile) {
|
||||
|
||||
bool test_done = false;
|
||||
|
||||
w.run([&]() -> et::task<> {
|
||||
w.run([&]() -> kota::task<> {
|
||||
worker::QueryParams params;
|
||||
params.kind = worker::QueryKind::SemanticTokens;
|
||||
params.path = "/tmp/nonexistent.cpp";
|
||||
@@ -236,7 +235,7 @@ TEST_CASE(FoldingRangeWithoutCompile) {
|
||||
|
||||
bool test_done = false;
|
||||
|
||||
w.run([&]() -> et::task<> {
|
||||
w.run([&]() -> kota::task<> {
|
||||
worker::QueryParams params;
|
||||
params.kind = worker::QueryKind::FoldingRange;
|
||||
params.path = "/tmp/nonexistent.cpp";
|
||||
@@ -257,7 +256,7 @@ TEST_CASE(DocumentSymbolWithoutCompile) {
|
||||
|
||||
bool test_done = false;
|
||||
|
||||
w.run([&]() -> et::task<> {
|
||||
w.run([&]() -> kota::task<> {
|
||||
worker::QueryParams params;
|
||||
params.kind = worker::QueryKind::DocumentSymbol;
|
||||
params.path = "/tmp/nonexistent.cpp";
|
||||
@@ -278,7 +277,7 @@ TEST_CASE(DocumentLinkWithoutCompile) {
|
||||
|
||||
bool test_done = false;
|
||||
|
||||
w.run([&]() -> et::task<> {
|
||||
w.run([&]() -> kota::task<> {
|
||||
worker::QueryParams params;
|
||||
params.kind = worker::QueryKind::DocumentLink;
|
||||
params.path = "/tmp/nonexistent.cpp";
|
||||
@@ -299,7 +298,7 @@ TEST_CASE(InlayHintsWithoutCompile) {
|
||||
|
||||
bool test_done = false;
|
||||
|
||||
w.run([&]() -> et::task<> {
|
||||
w.run([&]() -> kota::task<> {
|
||||
worker::QueryParams params;
|
||||
params.kind = worker::QueryKind::InlayHints;
|
||||
params.path = "/tmp/nonexistent.cpp";
|
||||
@@ -330,7 +329,7 @@ TEST_CASE(MultipleSequentialRequests) {
|
||||
|
||||
bool test_done = false;
|
||||
|
||||
w.run([&]() -> et::task<> {
|
||||
w.run([&]() -> kota::task<> {
|
||||
// Compile first so feature requests return real data.
|
||||
worker::CompileParams cp;
|
||||
cp.path = src;
|
||||
@@ -402,7 +401,7 @@ TEST_CASE(MultipleDocuments) {
|
||||
|
||||
bool test_done = false;
|
||||
|
||||
w.run([&]() -> et::task<> {
|
||||
w.run([&]() -> kota::task<> {
|
||||
// Compile 3 different documents.
|
||||
for(int i = 0; i < 3; i++) {
|
||||
worker::CompileParams cp;
|
||||
@@ -440,7 +439,7 @@ TEST_CASE(EvictNotification) {
|
||||
|
||||
bool test_done = false;
|
||||
|
||||
w.run([&]() -> et::task<> {
|
||||
w.run([&]() -> kota::task<> {
|
||||
// Send an evict notification — worker should remove the document without crashing.
|
||||
worker::EvictParams ep;
|
||||
ep.path = "/tmp/evict_test.cpp";
|
||||
@@ -474,7 +473,7 @@ TEST_CASE(SpawnWithMemoryLimit) {
|
||||
|
||||
bool test_done = false;
|
||||
|
||||
w.run([&]() -> et::task<> {
|
||||
w.run([&]() -> kota::task<> {
|
||||
// Compile first.
|
||||
worker::CompileParams cp;
|
||||
cp.path = src;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user