30 Commits

Author SHA1 Message Date
Myriad-Dreamin
1f8168577b wip 2026-04-20 23:10:08 +08:00
Myriad-Dreamin
60cab39f92 dev: make server public 2026-04-20 23:10:08 +08:00
Myriad-Dreamin
67bedd1ae0 dev: index nodes 2026-04-20 23:10:08 +08:00
Myriad-Dreamin
7ab8ad9513 fix: crash 2026-04-20 23:10:08 +08:00
Myriad-Dreamin
c11178fd77 dev: builtin lib 2026-04-20 23:10:08 +08:00
Myriad-Dreamin
150f47c590 hardcode clang 2026-04-20 23:10:08 +08:00
Myriad-Dreamin
b7987fded3 dev: workaround cc1 compilation 2026-01-26 17:45:00 +08:00
Myriad-Dreamin
44a4bd4107 dev: index notify 2026-01-26 17:44:47 +08:00
Myriad-Dreamin
044e4c4b27 feat: implement serialize for optional 2026-01-26 17:44:47 +08:00
Myriad-Dreamin
68eb63ba04 ActiveFile 2026-01-26 17:44:47 +08:00
Myriad-Dreamin
39d3648fdd feat: add method to check indices size 2026-01-26 17:43:16 +08:00
Myriad-Dreamin
6640c05d66 build: cannot build cpptrace 2026-01-26 17:43:16 +08:00
Myriad-Dreamin
69ac764ef2 feat: split copy_headers for external builds 2026-01-26 17:43:16 +08:00
Myriad-Dreamin
04c6ca5337 fix: correct server ref impl 2026-01-26 17:43:16 +08:00
Myriad-Dreamin
2408978d2d fix: correct plugin_paths decl 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
74f75f107f fix: compile error 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
0e2e487bc9 feat: introduce any type alias for llvm::json::Value 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
6be48bccd2 feat: implement dummy workspace/executeCommand 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
4262734d21 dev: simplify protocol def 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
ed0d7db3db dev: simplify protocol def 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
824b305f93 dev: simplify protocol def 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
f89793c66d dev: more lifetime hooks 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
4425fb5244 dev: simplify 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
25a85a3b8e feat: implement apis 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
0f95344abe fix: typo 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
3511915886 dev: remove useless comments 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
6e50451c43 fix: typo 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
99d9363b95 fix: paths 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
7533d4d15e docs: compile and laod plugin sections 2026-01-26 01:01:39 +08:00
Myriad-Dreamin
a118c16e96 feat: add minimal support to clice server plugins 2026-01-26 01:01:39 +08:00
537 changed files with 21626 additions and 46212 deletions

View File

@@ -100,7 +100,7 @@ SortIncludes: true
SortUsingDeclarations: Never SortUsingDeclarations: Never
IncludeBlocks: Regroup IncludeBlocks: Regroup
IncludeCategories: IncludeCategories:
- Regex: '^["<](spdlog|toml\+\+|coraing|cpptrace|flatbuffers|kota)/' - Regex: '^["<](spdlog|toml\+\+|coraing|cpptrace|flatbuffers)/'
Priority: 30 Priority: 30
SortPriority: 31 SortPriority: 31
@@ -139,15 +139,3 @@ KeepEmptyLines:
AtEndOfFile: false AtEndOfFile: false
AtStartOfBlock: false AtStartOfBlock: false
AtStartOfFile: false AtStartOfFile: false
StatementMacros:
- DECO_CFG_START
- DECO_CFG
- DECO_CFG_END
- DecoKV
- DecoFlag
- DecoComma
- DecoInput
- DecoPack
- DecoKVStyled
- DecoMulti

View File

@@ -1,50 +0,0 @@
---
Checks: >
-*,
bugprone-*,
modernize-*,
performance-*,
readability-*,
-modernize-use-trailing-return-type,
-readability-magic-numbers,
-readability-else-after-return,
-readability-braces-around-statements,
-readability-avoid-const-params-in-decls,
-readability-named-parameter,
-readability-implicit-bool-conversion,
-readability-use-anyofallof,
-bugprone-easily-swappable-parameters,
-bugprone-exception-escape,
-bugprone-narrowing-conversions,
-modernize-use-nodiscard,
WarningsAsErrors: ""
HeaderFilterRegex: "(src|tests)/.*"
CheckOptions:
# Naming conventions matching project style
- key: readability-identifier-naming.ClassCase
value: CamelCase
- key: readability-identifier-naming.StructCase
value: CamelCase
- key: readability-identifier-naming.EnumCase
value: CamelCase
- key: readability-identifier-naming.EnumConstantCase
value: CamelCase
- key: readability-identifier-naming.TemplateParameterCase
value: CamelCase
- key: readability-identifier-naming.TypeAliasCase
value: CamelCase
- key: readability-identifier-naming.FunctionCase
value: lower_case
- key: readability-identifier-naming.MethodCase
value: lower_case
- key: readability-identifier-naming.VariableCase
value: lower_case
- key: readability-identifier-naming.ParameterCase
value: lower_case
- key: readability-identifier-naming.MemberCase
value: lower_case
- key: readability-identifier-naming.NamespaceCase
value: lower_case

View File

@@ -1,268 +0,0 @@
# clice — Project Guide
## Project Overview
clice is a next-generation C++ language server (LSP) built on LLVM/Clang, targeting modern C++ (C++20/23). It uses a multi-process architecture with a master server coordinating stateless and stateful workers.
## Core Correction Patterns — Lessons from Past Interactions
The following patterns were extracted from extensive real-world collaboration. These are recurring mistakes that MUST be avoided. Read them carefully — they represent hard-won lessons, not hypothetical concerns.
### Pattern 1: Misjudging Real-World Priorities
AI tends to optimize whatever metric looks most impressive, rather than what actually matters in the user's real scenario.
**Example**: During performance optimization, the AI proudly reported "hot cache is 4-7x faster!" — but the function in question runs at LSP server startup, which is ALWAYS a cold start. Optimizing hot cache was completely meaningless.
**Rule**: Before optimizing or analyzing anything, first understand the REAL usage scenario. Ask yourself: "When does this code actually run? What does the user actually experience?" Do not chase metrics that look good on paper but are irrelevant in practice.
### Pattern 2: Pushing Without Local Verification
The most common and most damaging pattern. AI proposes a fix, pushes it immediately, CI fails, then another fix, push, fail again — wasting CI cycles and the user's time.
**Rule**: NEVER push code that you haven't verified locally. Before every push:
- Build locally with the same configuration CI uses.
- Run the relevant tests locally and confirm they pass.
- If you cannot reproduce the CI environment locally, say so — do not just "try and see."
- "It compiles" is NOT sufficient. Tests must pass.
### Pattern 3: Superficial Refactoring
When asked to refactor, AI tends to do mechanical code movement (copy functions from A to B) without understanding the deeper design intent (ownership, responsibility boundaries, API cleanliness).
**Example**: When splitting `MasterServer` into `Workspace` and `Session`, the AI moved functions but kept ugly APIs like `f(path_id, sessions_map)` instead of the clean `f(Session&)` that the refactoring was meant to achieve.
**Rule**: When refactoring, understand the WHY. Ask: "What design problem is this refactoring solving?" If you're just moving code around without improving the abstractions, you're not refactoring — you're rearranging deck chairs.
### Pattern 4: Fixing Only the Immediate Instance, Not the Pattern
When given a cleanup instruction, AI applies it to the single file or function currently being discussed, ignoring all other occurrences in the project.
**Example**: User says "remove decorative `===` comment separators." AI removes them from `workspace.cpp` only. User has to say: "The other files too!"
**Rule**: When given a cleanup or style instruction, apply it project-wide. Use `Grep` to find ALL occurrences and fix them all in one pass. Think: "Where else does this pattern appear?"
### Pattern 5: Never Skip, Disable, or Work Around Failing Tests
When stuck on a difficult bug (especially flaky CI, race conditions, platform-specific issues), AI may propose marking tests as `continue-on-error`, skipping them, or adding `expected-failure` annotations to make CI green.
**Rule**: This is ABSOLUTELY FORBIDDEN. If a test fails, fix the root cause. There are ZERO exceptions. Skipping a test to make CI green is not "fixing" — it is hiding a bug. If you ever find yourself thinking "maybe we should just skip this test," stop and reconsider your approach entirely.
### Pattern 6: Excessive Confirmation Seeking vs. Premature Execution
AI oscillates between two extremes: asking "should I do X?" for every trivial decision, or silently executing major changes without confirmation.
**Rule**: Calibrate based on reversibility and impact:
- **Small, reversible changes** (formatting, renaming a local variable, adding a test): just do it.
- **Architecture decisions, API changes, large refactors**: propose the plan first, wait for confirmation.
- **Pushing to remote, creating PRs, modifying CI**: always confirm.
- When the user says "go ahead" or "do it," execute fully without asking again mid-way.
## Code Reuse & Understanding Before Implementation
**This is the single most important rule in this project.** Before writing ANY new code, you MUST thoroughly read and understand the existing codebase first. This project has a rich set of utilities, abstractions, and patterns already in place — duplicating them wastes effort and creates maintenance burden.
Concrete requirements:
1. **Read before you write.** Before implementing a feature or fix, explore the relevant modules in `src/`. Search for existing helpers, utilities, and patterns that solve the same or similar problems. Use `Grep`, `Glob`, and `Agent` tools to investigate thoroughly — do not assume something doesn't exist just because you haven't seen it yet.
2. **Reuse existing infrastructure.** This project already has:
- A `Lexer` class (`src/syntax/lexer.h`) — do not hand-write token scanning logic.
- A `PositionMapper` for source location conversion — do not reimplement offset-to-line/column math.
- `CompilationUnitRef` methods (`decompose_location`, `decompose_range`, `file_path`, `directives`, etc.) — use them instead of raw Clang APIs.
- `SemanticVisitor` for AST traversal — extend it, do not write custom recursive AST walkers.
- `Tester` framework for unit tests with VFS, annotation support, and multi-phase compilation — use it, do not create ad-hoc test setups.
- Utility functions in `src/support/` — check there before writing new helpers.
3. **Follow established patterns.** When adding a new feature (e.g., a new LSP request handler), look at how 2-3 existing features of the same kind are implemented. Match their structure: same file organization, same function signatures, same error handling patterns. If every other feature in `src/feature/` follows a certain pattern, yours should too.
4. **Do not reinvent what the project already has.** If you find yourself writing a helper function that feels generic (string manipulation, path handling, JSON serialization, source range conversion), STOP and search the codebase first. There is a high probability it already exists. Creating duplicates leads to inconsistencies and bugs when one copy gets updated but the other doesn't.
5. **When in doubt, ask.** If you're unsure whether an existing utility covers your use case or whether to extend an existing abstraction vs. create a new one, ask the user rather than guessing.
## Source Layout
- `src/server/` — LSP server core: master server, compiler, indexer, stateful/stateless workers
- `src/feature/` — LSP feature implementations: hover, completion, document links, semantic tokens, etc.
- `src/compile/` — Compilation orchestration: compilation unit, directives, diagnostics
- `src/index/` — Symbol indexing: TUIndex, ProjectIndex, MergedIndex, include graph
- `src/semantic/` — Semantic analysis: symbol kinds, relations, AST visitor, template resolver
- `src/syntax/` — Lexer, scanner, token types, dependency graph
- `src/command/` — CLI parsing, compilation database, toolchain detection
- `src/support/` — Utilities: logging, filesystem, JSON, string helpers
## Build System
- Uses **pixi** for environment management and **CMake + Ninja** for building.
- Two build types: `Debug` and `RelWithDebInfo` (default).
- Build output goes to `build/[type]/`.
- See `/build`, `/test`, `/format` commands for common operations.
## Commit Message Format
Use **conventional commits** — enforced by CI:
```
<type>(<scope>): <short description>
```
- **Types**: `feat`, `fix`, `refactor`, `chore`, `docs`, `ci`, `test`
- **Scopes**: match `src/` subdirectories or feature names, e.g. `completion`, `server`, `index`, `tests`, `document links`
- Keep the subject line under 70 characters.
## Tests
Three types of tests, all must pass before committing:
- **Unit tests** (`tests/unit/`): C++ tests using the project's own test framework. Test names should be at most 4 words.
- **Integration tests** (`tests/integration/`): Python pytest tests that start a real clice server and communicate via LSP.
- **Smoke tests** (`tests/smoke/`): Replay recorded LSP sessions via `tests/replay.py`.
### Integration Test Style
- Keep tests concise. Do NOT write large comment blocks explaining the test layout or expected behavior.
- Use descriptive test function names and short inline comments only where logic is non-obvious.
## Pre-PR Review
Before opening a PR, launch **3 parallel subagents** to review the diff independently:
1. **Correctness reviewer**: Check for logic errors, edge cases, undefined behavior, and off-by-one mistakes.
2. **Style reviewer**: Verify the code follows this project's naming conventions, coding style, and CLAUDE.md rules.
3. **Test reviewer**: Confirm test coverage is adequate — new functionality has tests, edge cases are covered, and no existing tests were broken or weakened.
Each agent should read the full diff (`git diff main...HEAD`) and report issues. Fix all reported issues before opening the PR.
## Pre-commit Checklist
Before committing code, you MUST:
1. **Run `pixi run format`** to format all source files.
2. **Pass all three types of tests:**
- Unit tests: `pixi run unit-test [type]`
- Integration tests: `pixi run integration-test [type]`
- Smoke tests: `pixi run smoke-test [type]`
3. **All test failures must be fixed before committing.** This is a HARD REQUIREMENT with NO exceptions:
- If a test fails, it MUST be fixed before you commit. Do NOT commit with known failures.
- Do NOT skip, disable, or mark tests as expected-failure to work around breakage.
- Do NOT argue "this test was already broken before my changes" — if it fails on your branch, it is YOUR responsibility to fix it before committing. The main branch CI is green; any failure on your branch is caused by your changes, period.
- Do NOT defer fixing to a follow-up PR. Fix it NOW, in this branch, before committing.
---
## C++ Coding Style
### Template & Type Traits
- Do NOT blindly add `std::remove_cvref_t` on every template parameter. Understand C++ template argument deduction rules:
- `template<typename T> void f(T x)``T` is always deduced as a non-reference, non-cv-qualified type. No need for `remove_cvref_t`.
- `template<typename T> void f(T& x)``T` is deduced as the referred-to type (possibly cv-qualified, but never a reference). No need for `remove_cvref_t` to strip references.
- `template<typename T> void f(const T& x)``T` is deduced as a non-const, non-reference type. No need for `remove_cvref_t`.
- `template<typename T> void f(T&& x)`**forwarding reference**: `T` CAN be deduced as an lvalue reference (e.g., `int&`). This is the ONLY case where `std::remove_cvref_t<T>` is needed to get the bare type.
- Class template parameters and return types are also never deduced as references; don't add `remove_cvref_t` on them either.
### Type Traits & Concepts (C++20/23)
- This project targets C++20/23. Use variable templates directly for type traits — do NOT use the old pattern of wrapping a class template static member in a variable template. Prefer:
```cpp
// Good: directly specialize a variable template
template<typename T>
inline constexpr bool is_my_type_v = false;
template<>
inline constexpr bool is_my_type_v<MyType> = true;
```
```cpp
// Bad: unnecessary class template wrapper
template<typename T>
struct is_my_type : std::false_type {};
template<>
struct is_my_type<MyType> : std::true_type {};
template<typename T>
inline constexpr bool is_my_type_v = is_my_type<T>::value;
```
- When defining a concept that checks a type trait, do NOT add `std::remove_cvref_t` unless you specifically intend the concept to see through references/cv-qualifiers. If the concept is meant for a bare type, just use `T` directly — the caller is responsible for passing the right type.
```cpp
// Good
template<typename T>
concept MyTrait = is_my_type_v<T>;
// Bad: unnecessary remove_cvref_t
template<typename T>
concept MyTrait = is_my_type_v<std::remove_cvref_t<T>>;
```
### Naming Conventions
- **Variables, member fields, function names**: `snake_case`. Class member fields do NOT use any special suffix/prefix (no trailing `_`, no `m_` prefix).
- **Class names, template parameter names, enum names**: `PascalCase`. Exception: some class names also use `snake_case` — follow the existing style in the project.
- **Enum values**: `PascalCase`.
### String Literals
- Prefer C++11 raw string literals `R"(...)"` over escaped strings. Avoid `\"`, `\\`, `\n` in string literals when a raw literal is cleaner.
### Error Handling
- **Prefer `if` with init-statements to tightly scope error variables**, but avoid them when they compromise code readability or flatten control flow.
- **Omit redundant conditions:** If the error type provides an `operator bool` or evaluates implicitly (e.g., standard error codes, custom error wrappers), omit the redundant condition check.
- **Avoid forced `else` branches:** If scoping the variable inside the `if` requires you to introduce an `else` block for the success path (especially when returning early on error), declare the variable in the local scope instead to keep the control flow flat.
```cpp
// Good: Omit redundant condition when the type has operator bool
if (auto err = foo()) {
/* handle error */
}
// Bad: Redundant condition check
if (auto err = foo(); err) {
/* handle error */
}
// Good: Use init-statement when a custom condition is required,
// AND the variable isn't needed outside the if-statement
if (auto result = foo(); !result.has_value()) {
/* handle error */
}
// --- Scope and Control Flow Considerations ---
// Bad: Using init-statement forces an 'else' block because 'result'
// goes out of scope, leading to nested/redundant code.
if (auto result = get_data(); !result.has_value()) {
return result.error();
} else {
process(result.value()); // Success path is forced into a nested block
}
// Good: Declare as a regular local variable to allow early exit
// and keep the success path un-nested (flat control flow).
auto result = get_data();
if (!result.has_value()) {
return result.error();
}
process(result.value());
```
### Style
- Prefer `[[maybe_unused]]` over `(void)` for intentionally unused variables or parameters.
### Modern C++ Usage
- Use C++20/23 APIs whenever possible. Do NOT use `<iostream>` facilities (`std::cout`, `std::cin`, `std::cerr`, etc.). Also do NOT use C-style I/O (`printf`, `fprintf`, etc.).
- Prefer `std::ranges` / `std::views` APIs over raw loops and traditional `<algorithm>` calls.
- If the project depends on LLVM, prefer LLVM's efficient data structures (e.g., `llvm::SmallVector`, `llvm::DenseMap`, `llvm::StringMap`, `llvm::StringRef`) over their `std` counterparts when appropriate.
### Parameter Passing Preferences
- For string parameters, prefer `llvm::StringRef` > `std::string_view` > `const std::string&`.
- For array/span parameters, prefer `llvm::ArrayRef` > `std::span` > `const std::vector&`.

View File

@@ -1,15 +0,0 @@
Build the project. Accepts an optional argument for build type: `Debug` or `RelWithDebInfo` (default).
Available build commands:
- CMake configure only: `pixi run cmake-config [type]`
- CMake build only (skip configure): `pixi run cmake-build [type]`
- Full build (configure + build): `pixi run build [type]`
- Build a specific target: `pixi run cmake-build [type]` then `cmake --build build/[type] --target [target]`
Common targets: `clice`, `unit_tests`
Example usage:
- `/build` — full build RelWithDebInfo
- `/build Debug` — full build Debug

View File

@@ -1,5 +0,0 @@
Format all project source files.
Run: `pixi run format`
Formats C++, Python, Lua, JS/TS, Markdown, JSON, TOML, and YAML files.

View File

@@ -1,19 +0,0 @@
Run tests. Accepts an optional argument for build type: `Debug` or `RelWithDebInfo` (default).
Available test commands:
- Unit tests: `pixi run unit-test [type]`
- Integration tests: `pixi run integration-test [type]`
- Smoke tests: `pixi run smoke-test [type]`
- All tests (unit + integration): `pixi run test [type]`
Filtering specific tests:
- Unit tests: `pixi run unit-test [type] --test-filter=SuiteName.CaseName`
- Integration tests: `pixi run pytest tests/integration -k "test_name" --executable=./build/[type]/bin/clice`
- Smoke tests: `pixi run python tests/replay.py tests/smoke/specific.jsonl --clice=./build/[type]/bin/clice`
Example usage:
- `/test` — run all tests (RelWithDebInfo)
- `/test Debug` — run all tests (Debug)

View File

@@ -1,7 +1,2 @@
chat: chat:
auto_reply: false auto_reply: false
reviews:
auto_review:
enabled: true
summary:
enabled: false

7
.gitattributes vendored
View File

@@ -1,9 +1,2 @@
# SCM syntax highlighting & preventing 3-way merges # SCM syntax highlighting & preventing 3-way merges
pixi.lock merge=binary linguist-language=YAML linguist-generated=true -diff pixi.lock merge=binary linguist-language=YAML linguist-generated=true -diff
# Force LF line endings for test data so that byte offsets from clang
# (which reads from disk) match the content sent by didOpen in tests.
tests/data/** text eol=lf
# Treat trace files as binary to suppress text diffs
tests/smoke/*.jsonl linguist-generated=true binary

View File

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

View File

@@ -1,44 +0,0 @@
name: benchmark
on:
workflow_dispatch:
jobs:
benchmark:
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-15, windows-2025]
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: ./.github/actions/setup-pixi
- name: Build scan_benchmark
run: |
pixi run cmake-config RelWithDebInfo ON -- -DCLICE_ENABLE_BENCHMARK=ON
cmake --build build/RelWithDebInfo --target scan_benchmark
- name: Clone LLVM
run: git clone --depth 1 https://github.com/llvm/llvm-project.git
- name: Generate CDB
run: |
cmake -B llvm-build -G Ninja \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DCMAKE_TOOLCHAIN_FILE="$(pwd)/cmake/toolchain.cmake" \
-DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra;lld;lldb;mlir;polly;flang;bolt" \
-DLLVM_ENABLE_RUNTIMES="compiler-rt;libcxx;libcxxabi;libunwind" \
llvm-project/llvm
- name: Run benchmark
run: ./build/RelWithDebInfo/bin/scan_benchmark --runs 20 llvm-build/compile_commands.json
- name: Stop sccache server
if: runner.os == 'Windows'
run: pixi run -- sccache --stop-server || true

View File

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

View File

@@ -14,12 +14,6 @@ jobs:
with: with:
environments: format 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 - name: Run formatter
run: pixi run format run: pixi run format
continue-on-error: true continue-on-error: true
@@ -30,11 +24,10 @@ jobs:
args: --lint ./docs args: --lint ./docs
continue-on-error: true continue-on-error: true
- name: Check diff - name: Suggest changes
run: | if: github.event_name == 'pull_request'
if ! git diff --quiet; then uses: reviewdog/action-suggester@v1.24.0
echo "::error::Formatting changes detected. Please run 'pixi run format' and commit the result." with:
git --no-pager diff --stat tool_name: "fmt"
git --no-pager diff fail_level: any
exit 1 filter_mode: nofilter
fi

View File

@@ -20,11 +20,9 @@ jobs:
run: pixi run build-docs run: pixi run build-docs
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
uses: peaceiris/actions-gh-pages@v4
with: with:
personal_token: ${{ secrets.PUBLISH_DOCS }} github_token: ${{ secrets.GITHUB_TOKEN }}
external_repository: clice-io/docs publish_dir: docs/.vitepress/dist
publish_dir: ./docs/.vitepress/dist cname: clice.io
destination_dir: clice
keep_files: true

View File

@@ -7,10 +7,6 @@ on:
pull_request: pull_request:
branches: [main] branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs: jobs:
changes: changes:
if: ${{ !startsWith(github.ref, 'refs/tags/') }} if: ${{ !startsWith(github.ref, 'refs/tags/') }}
@@ -23,7 +19,7 @@ jobs:
clice: ${{ steps.filter.outputs.clice }} clice: ${{ steps.filter.outputs.clice }}
vscode: ${{ steps.filter.outputs.vscode }} vscode: ${{ steps.filter.outputs.vscode }}
cmake: ${{ steps.filter.outputs.cmake }} cmake: ${{ steps.filter.outputs.cmake }}
xmake: ${{ steps.filter.outputs.xmake }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dorny/paths-filter@v3 - uses: dorny/paths-filter@v3
@@ -50,34 +46,21 @@ jobs:
- 'tests/**' - 'tests/**'
- 'config/**' - 'config/**'
- '.github/workflows/test-cmake.yml' - '.github/workflows/test-cmake.yml'
xmake:
conventional-commit: - 'xmake.lua'
if: ${{ !startsWith(github.ref, 'refs/tags/') }} - 'src/**'
runs-on: ubuntu-latest - 'include/**'
steps: - 'tests/**'
- name: Check conventional commit format - 'config/**'
env: - '.github/workflows/test-xmake.yml'
IS_PR: ${{ github.event_name == 'pull_request' }}
PR_TITLE: ${{ github.event.pull_request.title }}
COMMIT_MSG: ${{ github.event.head_commit.message }}
run: |
pattern='^(feat|fix|refactor|chore|build|ci|docs|test|perf|style|revert)(\(.+\))?: .+'
if [[ "$IS_PR" == "true" ]]; then
subject="$PR_TITLE"
label="PR title"
else
subject=$(echo "$COMMIT_MSG" | head -n1)
label="Commit message"
fi
if [[ ! "$subject" =~ $pattern ]]; then
echo "::error::$label must follow conventional commit format: type(scope)?: description"
echo " Valid types: feat, fix, refactor, chore, build, ci, docs, test, perf, style, revert"
echo " Got: '$subject'"
exit 1
fi
format: format:
needs: changes needs: changes
permissions:
contents: read
checks: write
issues: write
pull-requests: write
if: ${{ needs.changes.outputs.format == 'true' }} if: ${{ needs.changes.outputs.format == 'true' }}
uses: ./.github/workflows/check-format.yml uses: ./.github/workflows/check-format.yml
@@ -87,12 +70,11 @@ jobs:
permissions: permissions:
contents: write contents: write
uses: ./.github/workflows/deploy-docs.yml uses: ./.github/workflows/deploy-docs.yml
secrets: inherit
# clice: clice:
# needs: changes needs: changes
# if: ${{ needs.changes.outputs.clice == 'true' }} if: ${{ needs.changes.outputs.clice == 'true' }}
# uses: ./.github/workflows/publish-clice.yml uses: ./.github/workflows/publish-clice.yml
vscode: vscode:
needs: changes needs: changes
@@ -104,6 +86,11 @@ jobs:
if: ${{ needs.changes.outputs.cmake == 'true' }} if: ${{ needs.changes.outputs.cmake == 'true' }}
uses: ./.github/workflows/test-cmake.yml uses: ./.github/workflows/test-cmake.yml
xmake:
needs: changes
if: ${{ needs.changes.outputs.xmake == 'true' }}
uses: ./.github/workflows/test-xmake.yml
release-clice: release-clice:
permissions: permissions:
contents: write contents: write
@@ -121,17 +108,16 @@ jobs:
checks-passed: checks-passed:
if: ${{ always() && !startsWith(github.ref, 'refs/tags/') }} if: ${{ always() && !startsWith(github.ref, 'refs/tags/') }}
needs: needs:
- conventional-commit
- format - format
- deploy - deploy
# - clice - clice
- vscode - vscode
- cmake - cmake
- xmake
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check results - name: Check results
uses: re-actors/alls-green@release/v1 uses: re-actors/alls-green@release/v1
with: with:
allowed-skips: conventional-commit,format,deploy,clice,vscode,cmake allowed-skips: format,deploy,clice,vscode,cmake,xmake
jobs: ${{ toJSON(needs) }} jobs: ${{ toJSON(needs) }}

View File

@@ -9,7 +9,6 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
# Native builds
- os: windows-2025 - os: windows-2025
artifact_name: clice.zip artifact_name: clice.zip
asset_name: clice-x64-windows-msvc.zip asset_name: clice-x64-windows-msvc.zip
@@ -28,63 +27,43 @@ jobs:
symbol_artifact_name: clice-symbol.tar.gz symbol_artifact_name: clice-symbol.tar.gz
symbol_asset_name: clice-arm64-macos-darwin-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 }} runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup xmake
uses: xmake-io/github-action-setup-xmake@v1
with:
xmake-version: 3.0.5
actions-cache-folder: ".xmake-cache"
actions-cache-key: ${{ matrix.os }}
package-cache: true
package-cache-key: ${{ matrix.os }}-pkg-release-v1
build-cache: true
build-cache-key: ${{ matrix.os }}-build-release-v1
- uses: ./.github/actions/setup-pixi - uses: ./.github/actions/setup-pixi
with: with:
environments: ${{ matrix.pixi_env || 'package' }} environments: package
- name: Package (native) - name: Remove ci llvm toolchain on Windows
if: ${{ !matrix.target_triple }} if: runner.os == 'Windows'
run: pixi run package
- name: Package (cross-compile)
if: ${{ matrix.target_triple }}
run: | run: |
ENV="${{ matrix.pixi_env }}" # @see https://github.com/xmake-io/xmake/issues/7158
pixi run -e "$ENV" package-config -- \ xmake lua os.rmdir "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Tools/Llvm"
"-DCLICE_TARGET_TRIPLE=${{ matrix.target_triple }}" xmake lua os.rmdir "C:/Program Files/LLVM"
pixi run -e "$ENV" cmake-build
- name: Package
run: pixi run package
- name: Upload Main Package to Release - name: Upload Main Package to Release
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
uses: svenstaro/upload-release-action@v2 uses: svenstaro/upload-release-action@v2
with: with:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
file: build/RelWithDebInfo/${{ matrix.artifact_name }} file: build/xpack/clice/${{ matrix.artifact_name }}
asset_name: ${{ matrix.asset_name }} asset_name: ${{ matrix.asset_name }}
tag: ${{ github.ref }} tag: ${{ github.ref }}
overwrite: true overwrite: true
@@ -94,7 +73,7 @@ jobs:
uses: svenstaro/upload-release-action@v2 uses: svenstaro/upload-release-action@v2
with: with:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
file: build/RelWithDebInfo/${{ matrix.symbol_artifact_name }} file: build/xpack/clice/${{ matrix.symbol_artifact_name }}
asset_name: ${{ matrix.symbol_asset_name }} asset_name: ${{ matrix.symbol_asset_name }}
tag: ${{ github.ref }} tag: ${{ github.ref }}
overwrite: true overwrite: true

View File

@@ -3,168 +3,27 @@ name: cmake
on: on:
workflow_call: workflow_call:
env:
CCACHE_DIR: ${{ github.workspace }}/.cache/ccache
SCCACHE_DIR: ${{ github.workspace }}/.cache/sccache
CCACHE_BASEDIR: ${{ github.workspace }}
SCCACHE_BASEDIRS: ${{ github.workspace }}
CCACHE_COMPILERCHECK: content
CCACHE_MAXSIZE: 2G
SCCACHE_CACHE_SIZE: 2G
jobs: jobs:
build: build:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
# Native builds
- os: windows-2025 - os: windows-2025
build_type: RelWithDebInfo build_type: RelWithDebInfo
- os: ubuntu-24.04 - os: ubuntu-24.04
build_type: Debug build_type: Debug
- os: ubuntu-24.04
build_type: RelWithDebInfo
- os: macos-15 - os: macos-15
build_type: Debug 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 }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- uses: ./.github/actions/setup-pixi - uses: ./.github/actions/setup-pixi
with:
environments: ${{ matrix.pixi_env || 'default' }}
- name: Restore compiler cache - name: Build
uses: actions/cache@v4
with:
path: ${{ runner.os == 'Windows' && '.cache/sccache' || '.cache/ccache' }}
key: ${{ runner.os }}-${{ matrix.build_type }}-${{ matrix.target_triple || 'native' }}-ccache-${{ github.sha }}
restore-keys: |
${{ 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 -e "$ENV" -- sccache --stop-server || true
pixi run -e "$ENV" -- sccache --zero-stats || true
else
pixi run -e "$ENV" -- ccache --zero-stats || true
fi
shell: bash
- name: Build (native)
if: ${{ !matrix.target_triple }}
run: pixi run build ${{ matrix.build_type }} ON run: pixi run build ${{ matrix.build_type }} ON
- name: Build (cross-compile) - name: Test
if: ${{ matrix.target_triple }} run: pixi run test ${{ matrix.build_type }}
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 tests
if: ${{ !matrix.build_only }}
timeout-minutes: 20
run: pixi run integration-test ${{ matrix.build_type }}
- 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 -e "$ENV" -- sccache --show-stats
pixi run -e "$ENV" -- sccache --stop-server || true
else
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 }}

42
.github/workflows/test-xmake.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: xmake
on:
workflow_call:
jobs:
build:
strategy:
fail-fast: false
matrix:
os: [windows-2025, ubuntu-24.04, macos-15]
build_type: [debug, releasedbg]
exclude:
- os: windows-2025
build_type: debug
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup xmake
uses: xmake-io/github-action-setup-xmake@v1
with:
xmake-version: 3.0.5
actions-cache-folder: ".xmake-cache"
actions-cache-key: ${{ matrix.os }}
package-cache: true
package-cache-key: ${{ matrix.os }}-pixi
build-cache: true
build-cache-key: ${{ matrix.os }}-${{ matrix.build_type }}
- uses: ./.github/actions/setup-pixi
- name: Build
run: pixi run xmake ${{ matrix.build_type }}
- name: Test
run: pixi run xmake-test
- name: Remove llvm package (Linux)
if: runner.os == 'Linux'
run: xmake require --uninstall clice-llvm

12
.gitignore vendored
View File

@@ -35,7 +35,7 @@
*build*/ *build*/
temp/ temp/
.cache/ .cache/
.xmake/
.llvm*/ .llvm*/
.clice/ .clice/
compile_commands.json compile_commands.json
@@ -56,19 +56,13 @@ __pycache__/
tests/unit/Local/ tests/unit/Local/
# IDEs & Editors # IDEs & Editors
/.vscode/* /.vscode/
!/.vscode/launch.json
!/.vscode/tasks.json
.vs/ .vs/
.idea/ .idea/
.claude
.clangd .clangd
# pixi environments # pixi environments
.env .env
.pixi/* .pixi/*
!.pixi/config.toml !.pixi/config.toml
.codex
.claude/*
!.claude/CLAUDE.md
!.claude/commands/

83
.vscode/launch.json vendored
View File

@@ -1,83 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug clice",
"program": "${workspaceFolder}/build/Debug/bin/clice",
"args": ["--mode=socket", "--port=50051"],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug clice (socket, RelWithDebInfo)",
"program": "${workspaceFolder}/build/RelWithDebInfo/bin/clice",
"args": ["--mode=socket", "--port=50051"],
"cwd": "${workspaceFolder}"
},
{
"name": "VSCode Extension (pipe)",
"type": "extensionHost",
"request": "launch",
"args": [
"--disable-extension=llvm-vs-code-extensions.vscode-clangd",
"--disable-extension=ms-vscode.cpptools",
"--disable-extension=ms-vscode.cpptools-extension-pack",
"--extensionDevelopmentPath=${workspaceFolder}/editors/vscode"
],
"env": {
"CLICE_MODE": "pipe"
},
"outFiles": ["${workspaceFolder}/editors/vscode/dist/**/*.js"],
"preLaunchTask": "npm: watch vscode ext"
},
{
"name": "VSCode Extension (socket)",
"type": "extensionHost",
"request": "launch",
"args": [
"--disable-extension=llvm-vs-code-extensions.vscode-clangd",
"--disable-extension=ms-vscode.cpptools",
"--disable-extension=ms-vscode.cpptools-extension-pack",
"--extensionDevelopmentPath=${workspaceFolder}/editors/vscode"
],
"env": {
"CLICE_MODE": "socket"
},
"outFiles": ["${workspaceFolder}/editors/vscode/dist/**/*.js"],
"preLaunchTask": "npm: watch vscode ext"
},
{
"type": "lldb",
"request": "launch",
"name": "Unit Test",
"program": "${workspaceFolder}/build/Debug/bin/unit_tests",
"args": ["--test-dir=./tests/data", "--test-filter=${input:filter}"],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Release Unit Test",
"program": "${workspaceFolder}/build/RelWithDebInfo/bin/unit_tests",
"args": ["--test-dir=./tests/data", "--test-filter=${input:filter}"],
"cwd": "${workspaceFolder}"
}
],
"compounds": [
{
"name": "clice + VSCode Extension (socket)",
"configurations": ["Debug clice", "VSCode Extension (socket)"],
"stopAll": true
}
],
"inputs": [
{
"id": "filter",
"type": "promptString",
"description": "Unit Test Filter"
}
]
}

42
.vscode/tasks.json vendored
View File

@@ -1,42 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "npm: install vscode deps",
"type": "shell",
"command": "pnpm",
"args": ["install"],
"options": {
"cwd": "${workspaceFolder}/editors/vscode"
},
"problemMatcher": []
},
{
"label": "npm: watch vscode ext",
"type": "shell",
"command": "pnpm",
"args": ["run", "watch"],
"options": {
"cwd": "${workspaceFolder}/editors/vscode"
},
"dependsOn": "npm: install vscode deps",
"problemMatcher": "$ts-webpack-watch",
"isBackground": true,
"presentation": {
"reveal": "never",
"group": "watchers"
}
},
{
"label": "npm: package vscode ext",
"type": "shell",
"command": "pnpm",
"args": ["run", "package"],
"options": {
"cwd": "${workspaceFolder}/editors/vscode"
},
"dependsOn": "npm: install vscode deps",
"problemMatcher": []
}
]
}

View File

@@ -13,45 +13,14 @@ set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/lib")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bin") set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bin")
option(CLICE_ENABLE_LTO "Enable ThinLTO for all targets" OFF) option(CLICE_ENABLE_LTO "Enable ThinLTO for all targets" OFF)
option(CLICE_USE_LIBCXX "Use libc++ instead of libstdc++" OFF)
option(CLICE_OFFLINE_BUILD "Disable network downloads during configuration" OFF)
option(CLICE_ENABLE_TEST "Build unit tests" OFF)
option(CLICE_CI_ENVIRONMENT "Enable CI-specific configuration" OFF)
option(CLICE_ENABLE_BENCHMARK "Build benchmarks" OFF)
option(CLICE_RELEASE "Enable release packaging (LTO + strip + pack)" OFF)
# Global flags that apply to all targets (including FetchContent dependencies).
if(NOT MSVC)
add_compile_options(-ffunction-sections -fdata-sections)
endif()
if(APPLE)
# https://conda-forge.org/docs/maintainer/knowledge_base/#newer-c-features-with-old-sdk
add_compile_definitions(_LIBCPP_DISABLE_AVAILABILITY=1)
endif()
if(MSVC OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND
CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC"))
string(APPEND CMAKE_EXE_LINKER_FLAGS " -Wl,/OPT:REF")
string(APPEND CMAKE_SHARED_LINKER_FLAGS " -Wl,/OPT:REF")
elseif(APPLE)
string(APPEND CMAKE_EXE_LINKER_FLAGS " -Wl,-dead_strip")
string(APPEND CMAKE_SHARED_LINKER_FLAGS " -Wl,-dead_strip")
else()
string(APPEND CMAKE_EXE_LINKER_FLAGS " -static-libstdc++ -static-libgcc -Wl,--gc-sections")
string(APPEND CMAKE_SHARED_LINKER_FLAGS " -Wl,--gc-sections")
endif()
# Make sure all third libraries are affected by ABI related options
if(CLICE_USE_LIBCXX) if(CLICE_USE_LIBCXX)
string(APPEND CMAKE_CXX_FLAGS " -stdlib=libc++") string(APPEND CMAKE_CXX_FLAGS " -stdlib=libc++")
string(APPEND CMAKE_EXE_LINKER_FLAGS " -stdlib=libc++") string(APPEND CMAKE_EXE_LINKER_FLAGS " -stdlib=libc++")
string(APPEND CMAKE_SHARED_LINKER_FLAGS " -stdlib=libc++") string(APPEND CMAKE_SHARED_LINKER_FLAGS " -stdlib=libc++")
endif() endif()
if(CLICE_RELEASE)
set(CLICE_ENABLE_LTO ON)
endif()
if(CLICE_ENABLE_LTO) if(CLICE_ENABLE_LTO)
string(APPEND CMAKE_C_FLAGS " -flto=thin") string(APPEND CMAKE_C_FLAGS " -flto=thin")
string(APPEND CMAKE_CXX_FLAGS " -flto=thin") string(APPEND CMAKE_CXX_FLAGS " -flto=thin")
@@ -60,12 +29,17 @@ if(CLICE_ENABLE_LTO)
string(APPEND CMAKE_MODULE_LINKER_FLAGS " -flto=thin") string(APPEND CMAKE_MODULE_LINKER_FLAGS " -flto=thin")
endif() endif()
if(CMAKE_BUILD_TYPE STREQUAL "Debug") if(CMAKE_BUILD_TYPE STREQUAL "Debug")
add_compile_options(-fsanitize=address) add_compile_options(-fsanitize=address)
if(CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") if(NOT WIN32)
# clang-cl (MSVC frontend): manually link ASan runtime since clang-cl string(APPEND CMAKE_EXE_LINKER_FLAGS " -fsanitize=address")
# doesn't handle -fsanitize=address linking automatically. string(APPEND CMAKE_SHARED_LINKER_FLAGS " -fsanitize=address")
endif()
if(MSVC AND CMAKE_CXX_COMPILER_ID MATCHES "Clang")
execute_process( execute_process(
COMMAND ${CMAKE_CXX_COMPILER} --print-resource-dir COMMAND ${CMAKE_CXX_COMPILER} --print-resource-dir
OUTPUT_VARIABLE CLANG_RESOURCE_DIR OUTPUT_VARIABLE CLANG_RESOURCE_DIR
@@ -75,47 +49,27 @@ if(CMAKE_BUILD_TYPE STREQUAL "Debug")
link_directories(${ASAN_LIB_PATH}) link_directories(${ASAN_LIB_PATH})
set(ASAN_LINK_FLAGS "") set(ASAN_LINK_FLAGS "")
list(APPEND ASAN_LINK_FLAGS "clang_rt.asan_dynamic-x86_64.lib") list(APPEND ASAN_LINK_FLAGS "clang_rt.asan_dynamic-x86_64.lib")
list(APPEND ASAN_LINK_FLAGS "/wholearchive:clang_rt.asan_dynamic_runtime_thunk-x86_64.lib") list(APPEND ASAN_LINK_FLAGS "/wholearchive:clang_rt.asan_dynamic_runtime_thunk-x86_64.lib")
foreach(flag ${ASAN_LINK_FLAGS}) foreach(flag ${ASAN_LINK_FLAGS})
string(APPEND CMAKE_EXE_LINKER_FLAGS " ${flag}") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${flag}")
string(APPEND CMAKE_SHARED_LINKER_FLAGS " ${flag}") set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${flag}")
string(APPEND CMAKE_MODULE_LINKER_FLAGS " ${flag}") set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} ${flag}")
endforeach() endforeach()
else() endif()
string(APPEND CMAKE_EXE_LINKER_FLAGS " -fsanitize=address")
string(APPEND CMAKE_SHARED_LINKER_FLAGS " -fsanitize=address")
endif() endif()
if(WIN32) if(APPLE)
# Disable Identical COMDAT Folding in Debug to avoid ASan ODR false positives. # https://conda-forge.org/docs/maintainer/knowledge_base/#newer-c-features-with-old-sdk
string(APPEND CMAKE_EXE_LINKER_FLAGS " -Wl,/OPT:NOICF") string(APPEND CMAKE_CXX_FLAGS " -D_LIBCPP_DISABLE_AVAILABILITY=1")
string(APPEND CMAKE_SHARED_LINKER_FLAGS " -Wl,/OPT:NOICF")
endif()
endif() endif()
include("${PROJECT_SOURCE_DIR}/cmake/package.cmake") include("${PROJECT_SOURCE_DIR}/cmake/package.cmake")
# Project-specific options (not applied to third-party deps).
add_library(clice_options INTERFACE) add_library(clice_options INTERFACE)
if(MSVC OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND
CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC"))
target_compile_options(clice_options INTERFACE /GR- /EHs-c- /Zc:preprocessor)
else()
target_compile_options(clice_options INTERFACE
-fno-rtti
-fno-exceptions
-Wno-deprecated-declarations
$<$<COMPILE_LANG_AND_ID:CXX,Clang,AppleClang>:-Wno-undefined-inline>
)
endif()
if(WIN32)
target_link_libraries(clice_options INTERFACE version ntdll)
endif()
if(CLICE_ENABLE_TEST) if(CLICE_ENABLE_TEST)
target_compile_definitions(clice_options INTERFACE CLICE_ENABLE_TEST=1) target_compile_definitions(clice_options INTERFACE CLICE_ENABLE_TEST=1)
endif() endif()
@@ -124,91 +78,123 @@ if(CLICE_CI_ENVIRONMENT)
target_compile_definitions(clice_options INTERFACE CLICE_CI_ENVIRONMENT=1) target_compile_definitions(clice_options INTERFACE CLICE_CI_ENVIRONMENT=1)
endif() endif()
set(CLICE_CLANG_TIDY_MODULE_LIBRARIES) if(WIN32)
set(CLICE_MISSING_CLANG_TIDY_MODULES) target_link_libraries(clice_options INTERFACE version ntdll)
foreach(module IN LISTS CLICE_CLANG_TIDY_MODULE_COMPONENTS) endif()
find_library(CLICE_${module}_LIBRARY
NAMES "${module}" if(WIN32)
PATHS "${LLVM_INSTALL_PATH}/lib" set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")
NO_DEFAULT_PATH target_link_options(clice_options INTERFACE
-fuse-ld=lld-link
-Wl,/OPT:REF
#,/OPT:NOICF
)
elseif(APPLE)
target_link_options(clice_options INTERFACE
-fuse-ld=lld
-Wl,-dead_strip
)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_link_options(clice_options INTERFACE
-fuse-ld=lld
-static-libstdc++ -static-libgcc
-Wl,--gc-sections
) )
if(CLICE_${module}_LIBRARY)
list(APPEND CLICE_CLANG_TIDY_MODULE_LIBRARIES "${CLICE_${module}_LIBRARY}")
else()
list(APPEND CLICE_MISSING_CLANG_TIDY_MODULES "${module}")
endif()
endforeach()
if(CLICE_MISSING_CLANG_TIDY_MODULES)
message(STATUS "Clang-tidy module libraries not available: ${CLICE_MISSING_CLANG_TIDY_MODULES}")
else()
target_compile_definitions(clice_options INTERFACE CLICE_HAS_CLANG_TIDY_MODULES=1)
endif() endif()
set(FBS_SCHEMA_FILE "${PROJECT_SOURCE_DIR}/src/index/schema.fbs") if(MSVC OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND
set(GENERATED_HEADER "${PROJECT_BINARY_DIR}/generated/schema_generated.h") CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC"))
set(CLANG_TIDY_CONFIG_SOURCE_FILE "${PROJECT_SOURCE_DIR}/config/clang-tidy-config.h") target_compile_options(clice_options INTERFACE
set(CLANG_TIDY_CONFIG_GENERATED_FILE "${PROJECT_BINARY_DIR}/generated/clang-tidy-config.h") /GR-
/EHsc-
if(CMAKE_CROSSCOMPILING) /Zc:preprocessor
find_program(FLATC_EXECUTABLE flatc REQUIRED) )
set(FLATC_CMD "${FLATC_EXECUTABLE}")
else() else()
set(FLATC_CMD "$<TARGET_FILE:flatc>") target_compile_options(clice_options INTERFACE
-fno-rtti
-fno-exceptions
-Wno-deprecated-declarations
-Wno-undefined-inline
-ffunction-sections
-fdata-sections
)
endif() endif()
set(FBS_SCHEMA_FILE "${CMAKE_CURRENT_SOURCE_DIR}/include/Index/schema.fbs")
set(GENERATED_HEADER "${CMAKE_CURRENT_BINARY_DIR}/generated/schema_generated.h")
add_custom_command( add_custom_command(
OUTPUT "${GENERATED_HEADER}" OUTPUT ${GENERATED_HEADER}
COMMAND ${FLATC_CMD} --cpp -o "${PROJECT_BINARY_DIR}/generated" "${FBS_SCHEMA_FILE}" COMMAND $<TARGET_FILE:flatc> --cpp -o ${CMAKE_CURRENT_BINARY_DIR}/generated ${FBS_SCHEMA_FILE}
DEPENDS "${FBS_SCHEMA_FILE}" DEPENDS ${FBS_SCHEMA_FILE}
COMMENT "Generating C++ header from ${FBS_SCHEMA_FILE}" COMMENT "Generating C++ header from ${FBS_SCHEMA_FILE}"
) )
add_custom_target(generate_flatbuffers_schema DEPENDS "${GENERATED_HEADER}") add_custom_target(
generate_flatbuffers_schema
DEPENDS ${GENERATED_HEADER}
)
set(CONFIG_SOURCE_FILE "${CMAKE_CURRENT_SOURCE_DIR}/config/clang-tidy-config.h")
set(CONFIG_GENERATED_FILE "${CMAKE_CURRENT_BINARY_DIR}/generated/clang-tidy-config.h")
add_custom_command( add_custom_command(
OUTPUT "${CLANG_TIDY_CONFIG_GENERATED_FILE}" OUTPUT ${CONFIG_GENERATED_FILE}
COMMAND ${CMAKE_COMMAND} -E copy_if_different COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CONFIG_SOURCE_FILE} ${CONFIG_GENERATED_FILE}
"${CLANG_TIDY_CONFIG_SOURCE_FILE}" DEPENDS ${CONFIG_SOURCE_FILE}
"${CLANG_TIDY_CONFIG_GENERATED_FILE}" COMMENT "Generating C++ header from ${CONFIG_SOURCE_FILE}"
DEPENDS "${CLANG_TIDY_CONFIG_SOURCE_FILE}"
COMMENT "Generating C++ header from ${CLANG_TIDY_CONFIG_SOURCE_FILE}"
) )
add_custom_target(generate_clang_tidy_config DEPENDS "${CLANG_TIDY_CONFIG_GENERATED_FILE}") add_custom_target(
generate_config
file(GLOB_RECURSE CLICE_CORE_SOURCES CONFIGURE_DEPENDS "${PROJECT_SOURCE_DIR}/src/*.cpp") DEPENDS ${CONFIG_GENERATED_FILE}
add_library(clice-core STATIC ${CLICE_CORE_SOURCES})
add_library(clice::core ALIAS clice-core)
add_dependencies(clice-core generate_flatbuffers_schema generate_clang_tidy_config)
target_include_directories(clice-core PUBLIC
"${PROJECT_SOURCE_DIR}/src"
"${PROJECT_BINARY_DIR}/generated"
) )
target_link_libraries(clice-core PUBLIC
add_library(clice_core_deps INTERFACE)
target_include_directories(clice_core_deps INTERFACE
"${PROJECT_SOURCE_DIR}/include"
"${CMAKE_CURRENT_BINARY_DIR}/generated"
)
target_link_libraries(clice_core_deps INTERFACE
clice_options clice_options
llvm-libs libuv::libuv
spdlog::spdlog spdlog::spdlog
tomlplusplus::tomlplusplus
roaring::roaring roaring::roaring
flatbuffers flatbuffers
kota::ipc::lsp llvm-libs
kota::codec::toml
simdjson::simdjson
) )
if(CLICE_CLANG_TIDY_MODULE_LIBRARIES)
target_link_libraries(clice-core PUBLIC ${CLICE_CLANG_TIDY_MODULE_LIBRARIES}) add_library(clice_builtin_api INTERFACE)
endif() target_link_libraries(clice_builtin_api INTERFACE clice_core_deps)
include("${PROJECT_SOURCE_DIR}/cmake/builtin-libraries.cmake")
clice_include_builtin_library_modules()
file(GLOB_RECURSE CLICE_SOURCES CONFIGURE_DEPENDS
"${PROJECT_SOURCE_DIR}/src/AST/*.cpp"
"${PROJECT_SOURCE_DIR}/src/Async/*.cpp"
"${PROJECT_SOURCE_DIR}/src/Basic/*.cpp"
"${PROJECT_SOURCE_DIR}/src/Compiler/*.cpp"
"${PROJECT_SOURCE_DIR}/src/Index/*.cpp"
"${PROJECT_SOURCE_DIR}/src/Feature/*.cpp"
"${PROJECT_SOURCE_DIR}/src/Server/*.cpp"
"${PROJECT_SOURCE_DIR}/src/Support/*.cpp"
)
add_library(clice-core STATIC "${CLICE_SOURCES}")
add_dependencies(clice-core generate_flatbuffers_schema generate_config)
target_link_libraries(clice-core PUBLIC clice_core_deps)
clice_finalize_builtin_libraries(TARGET clice-core)
add_executable(clice "${PROJECT_SOURCE_DIR}/src/clice.cc") add_executable(clice "${PROJECT_SOURCE_DIR}/src/clice.cc")
target_link_libraries(clice PRIVATE clice::core kota::deco) target_link_libraries(clice PRIVATE clice-core)
install(TARGETS clice RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) install(TARGETS clice RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
add_custom_target(copy_clang_resource ALL message(STATUS "Copying resource directory for development build")
COMMAND ${CMAKE_COMMAND} -E copy_directory file(
"${LLVM_INSTALL_PATH}/lib/clang" COPY "${LLVM_INSTALL_PATH}/lib/clang"
"${PROJECT_BINARY_DIR}/lib/clang" DESTINATION "${PROJECT_BINARY_DIR}/lib"
COMMENT "Copying clang resource directory"
) )
install( install(
DIRECTORY "${LLVM_INSTALL_PATH}/lib/clang" DIRECTORY "${LLVM_INSTALL_PATH}/lib/clang"
@@ -217,35 +203,11 @@ install(
if(CLICE_ENABLE_TEST) if(CLICE_ENABLE_TEST)
file(GLOB_RECURSE CLICE_TEST_SOURCES CONFIGURE_DEPENDS file(GLOB_RECURSE CLICE_TEST_SOURCES CONFIGURE_DEPENDS
"${PROJECT_SOURCE_DIR}/tests/unit/*/*_tests.cpp" "${PROJECT_SOURCE_DIR}/tests/unit/*/*.cpp")
)
set(CLICE_TEST_SUPPORT_SOURCES
"${PROJECT_SOURCE_DIR}/tests/unit/test/annotation.cpp"
"${PROJECT_SOURCE_DIR}/tests/unit/test/tester.cpp"
)
add_executable(unit_tests add_executable(unit_tests
"${CLICE_TEST_SOURCES}"
"${PROJECT_SOURCE_DIR}/tests/unit/unit_tests.cc" "${PROJECT_SOURCE_DIR}/tests/unit/unit_tests.cc"
${CLICE_TEST_SOURCES}
${CLICE_TEST_SUPPORT_SOURCES}
) )
target_include_directories(unit_tests PRIVATE target_include_directories(unit_tests PUBLIC "${PROJECT_SOURCE_DIR}")
"${PROJECT_SOURCE_DIR}/src" target_link_libraries(unit_tests PRIVATE clice-core cpptrace::cpptrace)
"${PROJECT_SOURCE_DIR}/tests/unit"
)
target_link_libraries(unit_tests PRIVATE clice::core kota::zest kota::deco)
endif()
if(CLICE_ENABLE_BENCHMARK)
add_executable(scan_benchmark
"${PROJECT_SOURCE_DIR}/benchmarks/scan_benchmark.cpp"
)
target_include_directories(scan_benchmark PRIVATE
"${PROJECT_SOURCE_DIR}/src"
)
target_link_libraries(scan_benchmark PRIVATE clice::core kota::deco)
endif()
if(CLICE_RELEASE)
include("${PROJECT_SOURCE_DIR}/cmake/release.cmake")
endif() endif()

View File

@@ -5,7 +5,7 @@
![C++ Standard](https://img.shields.io/badge/C++-23-blue.svg) ![C++ Standard](https://img.shields.io/badge/C++-23-blue.svg)
[![GitHub license](https://img.shields.io/github/license/clice-io/clice)](https://github.com/clice-io/clice/blob/main/LICENSE) [![GitHub license](https://img.shields.io/github/license/clice-io/clice)](https://github.com/clice-io/clice/blob/main/LICENSE)
[![Actions status](https://github.com/clice-io/clice/workflows/CI/badge.svg)](https://github.com/clice-io/clice/actions) [![Actions status](https://github.com/clice-io/clice/workflows/CI/badge.svg)](https://github.com/clice-io/clice/actions)
[![Documentation](https://img.shields.io/badge/view-documentation-blue)](https://docs.clice.io/clice/) [![Documentation](https://img.shields.io/badge/view-documentation-blue)](https://clice.io)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/clice-io/clice) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/clice-io/clice)
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.gg/PA3UxW2VA3) [![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.gg/PA3UxW2VA3)
@@ -35,4 +35,4 @@ Download the latest `clice` binary from the [releases page](https://github.com/c
## Documentation ## Documentation
To learn more about building, installing, and configuring clice, or to dive deep into its features and architecture, please visit our official documentation at [**clice.io**](https://docs.clice.io/clice/). To learn more about building, installing, and configuring clice, or to dive deep into its features and architecture, please visit our official documentation at [**clice.io**](https://clice.io/).

View File

@@ -1,411 +0,0 @@
/// Benchmark for scan_dependency_graph on a real compilation database.
///
/// Usage:
/// scan_benchmark [OPTIONS] <compile_commands.json>
///
/// Example:
/// ./build/RelWithDebInfo/bin/scan_benchmark \
/// /home/ykiko/C++/clice/.llvm/build-debug/compile_commands.json
///
/// ./build/RelWithDebInfo/bin/scan_benchmark --log-level info --export graph.json \
/// /home/ykiko/C++/clice/.llvm/build-debug/compile_commands.json
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <fstream>
#include <map>
#include <numeric>
#include <print>
#include <set>
#include <thread>
#include "command/command.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"
using namespace clice;
struct BenchmarkOptions {
DecoKV(names = {"--log-level"}; help = "Log level: trace, debug, info, warn, error, off";
required = false;)
<std::string> log_level = "off";
DecoKV(names = {"--export"}; help = "Export dependency graph as JSON to this path";
required = false;)
<std::string> export_path;
DecoKV(names = {"--runs"}; help = "Number of cold start iterations"; required = false;)
<int> runs = 20;
DecoFlag(names = {"-h", "--help"}; help = "Show help message"; required = false;)
help;
DecoInput(meta_var = "CDB"; help = "Path to compile_commands.json"; required = false;)
<std::string> cdb_path;
};
struct FileNode {
std::string path;
std::string module_name;
std::vector<std::string> includes;
};
struct GraphExport {
std::vector<FileNode> files;
};
void export_graph_json(const PathPool& path_pool,
const DependencyGraph& graph,
llvm::StringRef output_path) {
// Build reverse module map: path_id -> module_name.
llvm::DenseMap<std::uint32_t, llvm::StringRef> path_to_module;
for(auto& [name, path_ids]: graph.modules()) {
for(auto path_id: path_ids) {
path_to_module[path_id] = name;
}
}
GraphExport export_data;
for(std::uint32_t id = 0; id < path_pool.paths.size(); id++) {
auto inc_ids = graph.get_all_includes(id);
if(inc_ids.empty()) {
continue;
}
FileNode node;
node.path = path_pool.paths[id].str();
auto mod_it = path_to_module.find(id);
if(mod_it != path_to_module.end()) {
node.module_name = mod_it->second.str();
}
for(auto flagged_id: inc_ids) {
auto raw_id = flagged_id & DependencyGraph::PATH_ID_MASK;
node.includes.push_back(path_pool.paths[raw_id].str());
}
export_data.files.push_back(std::move(node));
}
auto json = kota::codec::json::to_json(export_data);
if(!json) {
std::println(stderr, "Failed to serialize dependency graph");
return;
}
std::ofstream out(output_path.str());
if(!out) {
std::println(stderr, "Failed to open output file: {}", output_path);
return;
}
out << *json;
std::println("Graph exported to {} ({} files)", output_path, export_data.files.size());
}
void print_report(const ScanReport& report) {
std::println("===============================================================");
std::println(" Dependency Scan Report");
std::println("===============================================================");
// Timing.
std::println("");
std::println(" Time: {}ms", report.elapsed_ms);
std::println(" Waves: {}", report.waves);
// File counts.
std::println("");
std::println(" Files");
std::println(" Source files (from CDB): {}", report.source_files);
std::println(" Header files (discovered): {}", report.header_files);
std::println(" Total: {}", report.total_files);
std::println(" Modules: {}", report.modules);
// Include edges.
std::println("");
std::println(" Include Edges");
std::println(" Total: {}", report.total_edges);
std::println(" Unconditional: {}", report.unconditional_edges);
std::println(" Conditional: {} (inside #if/#ifdef)", report.conditional_edges);
// Resolution accuracy.
std::println("");
std::println(" Resolution");
std::println(" #include directives: {}", report.includes_found);
std::println(" Resolved: {}", report.includes_resolved);
auto unresolved_count = report.includes_found - report.includes_resolved;
std::println(" Unresolved: {}", unresolved_count);
if(report.includes_found > 0) {
double rate = 100.0 * static_cast<double>(report.includes_resolved) /
static_cast<double>(report.includes_found);
std::println(" Accuracy: {:.1f}%", rate);
}
// Wall-clock phase breakdown.
std::println("");
std::println(" Phase Breakdown (wall-clock)");
std::println(" Config extraction: {}ms (prewarm={}ms, loop={}ms)",
report.config_ms,
report.prewarm_ms,
report.config_loop_ms);
std::println(" Dir cache pre-pop: {}ms (overlapped with Phase 1)", report.dir_cache_ms);
std::println(" Phase 1 (read+scan, parallel): {}ms", report.phase1_ms);
std::println(" Phase 2 (include resolve): {}ms", report.phase2_ms);
std::println(" Phase 3 (graph build): {}ms", report.phase3_ms);
// Per-wave breakdown.
if(!report.wave_stats.empty()) {
std::println("");
std::println(" Per-Wave Breakdown");
std::println(" {:>5s} {:>8s} {:>8s} {:>8s} {:>8s} {:>8s} {:>10s} {:>10s}",
"Wave",
"Files",
"P1(ms)",
"P2(ms)",
"Next",
"Prefetch",
"DirList",
"DirHits");
for(std::size_t i = 0; i < report.wave_stats.size(); i++) {
auto& ws = report.wave_stats[i];
std::println(" {:>5} {:>8} {:>8} {:>8} {:>8} {:>8} {:>10} {:>10}",
i,
ws.files,
ws.phase1_ms,
ws.phase2_ms,
ws.next_files,
ws.prefetch_count,
ws.dir_listings,
ws.dir_hits);
}
}
// Phase 2 breakdown.
if(report.p2_resolve_us > 0) {
auto other_us = report.phase2_ms * 1000 - report.p2_resolve_us;
std::println("");
std::println(" Phase 2 Breakdown (single-threaded)");
std::println(" resolve_include: {:.1f}ms", report.p2_resolve_us / 1000.0);
std::println(" Other (cache lookup, intern, graph): {:.1f}ms", other_us / 1000.0);
}
// Cumulative I/O statistics.
std::println("");
std::println(" I/O Statistics (cumulative across threads)");
std::println(" File read: {:.1f}ms (sum of all threads)", report.read_us / 1000.0);
std::println(" Lexer scan: {:.1f}ms (sum of all threads)", report.scan_us / 1000.0);
std::println(" Filesystem: {:.1f}ms ({} readdir calls, {} dir cache hits)",
report.fs_us / 1000.0,
report.dir_listings,
report.dir_hits);
std::println(" File lookups: {}", report.fs_lookups);
std::println(" Include cache hits: {}", report.include_cache_hits);
std::println(" Scan result cache hits: {}", report.scan_cache_hits);
if(report.dir_listings + report.dir_hits > 0) {
double hit_rate = 100.0 * static_cast<double>(report.dir_hits) /
static_cast<double>(report.dir_listings + report.dir_hits);
std::println(" Dir cache hit rate: {:.1f}%", hit_rate);
}
std::println("");
std::println("===============================================================");
}
int main(int argc, const char** argv) {
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);
return 1;
}
auto& opts = result->options;
if(opts.help.value_or(false) || !opts.cdb_path.has_value()) {
std::ostringstream oss;
kota::deco::cli::write_usage_for<BenchmarkOptions>(oss, "scan_benchmark [OPTIONS] <cdb>");
std::print("{}", oss.str());
return opts.help.value_or(false) ? 0 : 1;
}
// Configure logging.
auto level = spdlog::level::from_str(*opts.log_level);
clice::logging::options.level = level;
clice::logging::stderr_logger("scan_benchmark", clice::logging::options);
// resource_dir() is self-initializing (lazy static) — no setup needed.
auto& cdb_path = *opts.cdb_path;
auto hw_threads = std::thread::hardware_concurrency();
auto runs = *opts.runs;
if(runs <= 0) {
std::println(stderr, "Error: --runs must be positive (got {})", runs);
return 1;
}
// Set UV_THREADPOOL_SIZE if not already set.
// Use at least libuv's default (4) so low-core CI runners don't regress.
if(!std::getenv("UV_THREADPOOL_SIZE")) {
auto pool_size = std::max(hw_threads, 4u);
static std::string env = "UV_THREADPOOL_SIZE=" + std::to_string(pool_size);
putenv(env.data());
}
std::println("Hardware threads: {}", hw_threads);
std::println("UV_THREADPOOL_SIZE: {}", std::getenv("UV_THREADPOOL_SIZE"));
std::println("Log level: {}", *opts.log_level);
std::println("CDB: {}", cdb_path);
std::println("");
// Load compilation database.
auto t0 = std::chrono::steady_clock::now();
CompilationDatabase cdb;
auto count = cdb.load(cdb_path);
auto t1 = std::chrono::steady_clock::now();
auto load_ms = std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0).count();
std::println("CDB loaded: {} entries in {}ms", count, load_ms);
{
std::set<const CompilationInfo*> unique_contexts;
std::set<const CanonicalCommand*> unique_canonicals;
std::map<const CanonicalCommand*, int> canonical_hist;
for(auto& entry: cdb.get_entries()) {
unique_contexts.insert(entry.info.ptr);
unique_canonicals.insert(entry.info->canonical.ptr);
canonical_hist[entry.info->canonical.ptr]++;
}
double dedup_ratio =
unique_contexts.empty() ? 0.0 : static_cast<double>(count) / unique_contexts.size();
std::println(
"Context dedup: {} files -> {} unique contexts ({:.1f}x), {} unique canonicals",
count,
unique_contexts.size(),
dedup_ratio,
unique_canonicals.size());
// If canonical dedup is poor, dump diagnostics.
if(unique_canonicals.size() > 200) {
// Sort canonicals by frequency (descending).
std::vector<std::pair<int, const CanonicalCommand*>> sorted;
for(auto& [ptr, cnt]: canonical_hist)
sorted.push_back({cnt, ptr});
std::ranges::sort(sorted,
std::greater{},
&std::pair<int, const CanonicalCommand*>::first);
// Show top-5 canonical commands.
for(int i = 0; i < std::min(5, (int)sorted.size()); i++) {
auto [cnt, cmd] = sorted[i];
std::println(" canonical[{}] ({} files, {} args):", i, cnt, cmd->arguments.size());
for(auto arg: cmd->arguments)
std::println(" {}", arg);
}
// Show a singleton canonical (count==1) to see what per-file arg leaks in.
for(auto& [cnt, cmd]: sorted) {
if(cnt == 1) {
std::println(" singleton canonical ({} args):", cmd->arguments.size());
for(auto arg: cmd->arguments)
std::println(" {}", arg);
break;
}
}
// Find two canonicals that differ by only a few args.
if(sorted.size() >= 2) {
auto* a = sorted[0].second;
auto* b = sorted[1].second;
std::println(" --- Canonical diff (top-1 vs top-2) ---");
auto max_len = std::max(a->arguments.size(), b->arguments.size());
for(std::size_t i = 0; i < max_len; i++) {
llvm::StringRef av = i < a->arguments.size() ? a->arguments[i] : "<missing>";
llvm::StringRef bv = i < b->arguments.size() ? b->arguments[i] : "<missing>";
if(av != bv)
std::println(" DIFF[{}]: '{}' vs '{}'", i, av, bv);
else
std::println(" SAME[{}]: '{}'", i, av);
}
}
}
}
std::println("\nRunning {} cold start scan(s)...\n", runs);
PathPool path_pool;
DependencyGraph graph;
std::vector<std::int64_t> elapsed_times;
std::vector<std::int64_t> config_times;
std::vector<std::int64_t> phase1_times;
std::vector<std::int64_t> phase2_times;
elapsed_times.reserve(runs);
config_times.reserve(runs);
phase1_times.reserve(runs);
phase2_times.reserve(runs);
for(int i = 0; i < runs; i++) {
// True cold start: rebuild CDB (clears toolchain & config caches),
// reset PathPool and DependencyGraph.
cdb = CompilationDatabase{};
cdb.load(cdb_path);
path_pool = PathPool{};
graph = DependencyGraph{};
auto report = scan_dependency_graph(cdb, path_pool, graph);
elapsed_times.push_back(report.elapsed_ms);
config_times.push_back(report.config_ms);
phase1_times.push_back(report.phase1_ms);
phase2_times.push_back(report.phase2_ms);
std::println("[run {:2}] {}ms | config={}ms phase1={}ms phase2={}ms | files={}",
i + 1,
report.elapsed_ms,
report.config_ms,
report.phase1_ms,
report.phase2_ms,
report.total_files);
// Print detailed report for the first run only.
if(i == 0) {
std::println("");
print_report(report);
}
}
// Summary statistics.
if(runs > 1) {
auto stats = [](std::vector<std::int64_t>& v) {
std::ranges::sort(v);
auto sum = std::accumulate(v.begin(), v.end(), std::int64_t{0});
return std::tuple{v.front(), sum / static_cast<std::int64_t>(v.size()), v.back()};
};
auto [e_min, e_avg, e_max] = stats(elapsed_times);
auto [c_min, c_avg, c_max] = stats(config_times);
auto [p1_min, p1_avg, p1_max] = stats(phase1_times);
auto [p2_min, p2_avg, p2_max] = stats(phase2_times);
std::println("\n Summary ({} runs) min avg max", runs);
std::println(" Total: {:>7} {:>6} {:>6}", e_min, e_avg, e_max);
std::println(" Config extraction: {:>7} {:>6} {:>6}", c_min, c_avg, c_max);
std::println(" Phase 1 (read+scan):{:>7} {:>6} {:>6}", p1_min, p1_avg, p1_max);
std::println(" Phase 2 (resolve): {:>7} {:>6} {:>6}", p2_min, p2_avg, p2_max);
}
// Export dependency graph as JSON if requested.
if(opts.export_path.has_value()) {
export_graph_json(path_pool, graph, *opts.export_path);
}
return 0;
}

View File

@@ -1,17 +0,0 @@
if(OUTPUT MATCHES "\\.tar\\.gz$")
execute_process(
COMMAND ${CMAKE_COMMAND} -E tar czf "${OUTPUT}" .
WORKING_DIRECTORY "${WORK_DIR}"
COMMAND_ERROR_IS_FATAL ANY
)
elseif(OUTPUT MATCHES "\\.zip$")
execute_process(
COMMAND ${CMAKE_COMMAND} -E tar cf "${OUTPUT}" --format=zip .
WORKING_DIRECTORY "${WORK_DIR}"
COMMAND_ERROR_IS_FATAL ANY
)
else()
message(FATAL_ERROR "Unsupported archive format: ${OUTPUT}")
endif()
message(STATUS "Created: ${OUTPUT}")

View File

@@ -0,0 +1,158 @@
include_guard(GLOBAL)
set(
CLICE_BUILTIN_LIBRARY_MODULES
"${CLICE_BUILTIN_LIBRARY_MODULES}"
CACHE STRING
"Semicolon-separated list of CMake modules that register extra builtin clice libraries"
)
function(clice_add_builtin_library)
set(options)
set(oneValueArgs NAME ENTRYPOINT)
set(multiValueArgs SOURCES INCLUDE_DIRECTORIES LINK_LIBRARIES COMPILE_DEFINITIONS COMPILE_OPTIONS)
cmake_parse_arguments(CBL "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if(CBL_UNPARSED_ARGUMENTS)
message(
FATAL_ERROR
"clice_add_builtin_library got unexpected arguments: ${CBL_UNPARSED_ARGUMENTS}"
)
endif()
if(NOT CBL_NAME)
message(FATAL_ERROR "clice_add_builtin_library requires NAME")
endif()
if(NOT CBL_SOURCES)
message(FATAL_ERROR "clice_add_builtin_library(${CBL_NAME}) requires SOURCES")
endif()
if(NOT CBL_ENTRYPOINT)
message(FATAL_ERROR "clice_add_builtin_library(${CBL_NAME}) requires ENTRYPOINT")
endif()
if(NOT TARGET clice_builtin_api)
message(
FATAL_ERROR
"clice_builtin_api must be defined before calling clice_add_builtin_library(${CBL_NAME})"
)
endif()
set(target "clice_builtin_${CBL_NAME}")
if(TARGET "${target}")
message(FATAL_ERROR "builtin library target '${target}' already exists")
endif()
add_library("${target}" OBJECT)
target_sources("${target}" PRIVATE ${CBL_SOURCES})
target_link_libraries("${target}" PUBLIC clice_builtin_api)
add_dependencies("${target}" generate_flatbuffers_schema generate_config)
if(CBL_INCLUDE_DIRECTORIES)
target_include_directories("${target}" PRIVATE ${CBL_INCLUDE_DIRECTORIES})
endif()
if(CBL_LINK_LIBRARIES)
target_link_libraries("${target}" PUBLIC ${CBL_LINK_LIBRARIES})
endif()
if(CBL_COMPILE_DEFINITIONS)
target_compile_definitions("${target}" PRIVATE ${CBL_COMPILE_DEFINITIONS})
endif()
if(CBL_COMPILE_OPTIONS)
target_compile_options("${target}" PRIVATE ${CBL_COMPILE_OPTIONS})
endif()
set_property(GLOBAL APPEND PROPERTY CLICE_BUILTIN_LIBRARY_TARGETS "${target}")
set_property(GLOBAL APPEND PROPERTY CLICE_BUILTIN_LIBRARY_ENTRYPOINTS "${CBL_ENTRYPOINT}")
endfunction()
function(clice_include_builtin_library_modules)
foreach(module_path IN LISTS CLICE_BUILTIN_LIBRARY_MODULES)
cmake_path(
ABSOLUTE_PATH module_path
BASE_DIRECTORY "${PROJECT_SOURCE_DIR}"
NORMALIZE
OUTPUT_VARIABLE module_abs_path
)
if(NOT EXISTS "${module_abs_path}")
message(
FATAL_ERROR
"builtin library module '${module_path}' does not exist: ${module_abs_path}"
)
endif()
include("${module_abs_path}")
endforeach()
endfunction()
function(clice_finalize_builtin_libraries)
set(options)
set(oneValueArgs TARGET REGISTRATION_SOURCE)
cmake_parse_arguments(CBL "${options}" "${oneValueArgs}" "" ${ARGN})
if(CBL_UNPARSED_ARGUMENTS)
message(
FATAL_ERROR
"clice_finalize_builtin_libraries got unexpected arguments: ${CBL_UNPARSED_ARGUMENTS}"
)
endif()
if(NOT CBL_TARGET)
message(FATAL_ERROR "clice_finalize_builtin_libraries requires TARGET")
endif()
if(NOT TARGET "${CBL_TARGET}")
message(FATAL_ERROR "target '${CBL_TARGET}' does not exist")
endif()
if(NOT CBL_REGISTRATION_SOURCE)
set(CBL_REGISTRATION_SOURCE "${CMAKE_CURRENT_BINARY_DIR}/generated/builtin-libraries.cpp")
endif()
get_property(builtin_targets GLOBAL PROPERTY CLICE_BUILTIN_LIBRARY_TARGETS)
get_property(builtin_entrypoints GLOBAL PROPERTY CLICE_BUILTIN_LIBRARY_ENTRYPOINTS)
if(NOT builtin_targets)
set(builtin_targets "")
endif()
if(NOT builtin_entrypoints)
set(builtin_entrypoints "")
endif()
set(registration_source_content "#include \"Server/Plugin.h\"\n\nnamespace clice {\n\n")
foreach(entrypoint IN LISTS builtin_entrypoints)
string(APPEND registration_source_content "::clice::PluginInfo ${entrypoint}();\n")
endforeach()
string(APPEND registration_source_content "\nvoid register_builtin_server_plugins(ServerPluginBuilder& builder) {\n")
foreach(entrypoint IN LISTS builtin_entrypoints)
string(
APPEND
registration_source_content
" ${entrypoint}().register_server_callbacks(builder);\n"
)
endforeach()
string(APPEND registration_source_content "}\n\n} // namespace clice\n")
get_filename_component(registration_source_dir "${CBL_REGISTRATION_SOURCE}" DIRECTORY)
file(MAKE_DIRECTORY "${registration_source_dir}")
file(CONFIGURE OUTPUT "${CBL_REGISTRATION_SOURCE}" CONTENT "${registration_source_content}" @ONLY)
target_sources("${CBL_TARGET}" PRIVATE "${CBL_REGISTRATION_SOURCE}")
if(builtin_targets)
foreach(builtin_target IN LISTS builtin_targets)
target_sources("${CBL_TARGET}" PRIVATE $<TARGET_OBJECTS:${builtin_target}>)
endforeach()
target_link_libraries("${CBL_TARGET}" PRIVATE ${builtin_targets})
endif()
endfunction()

View File

@@ -1,34 +1,5 @@
include_guard() include_guard()
set(CLICE_CLANG_TIDY_MODULE_COMPONENTS
# Keep this in sync with scripts/llvm-components.json and the old
# ALL_CLANG_TIDY_CHECKS list. MPIModule is intentionally excluded because
# clice disables static analyzer checks in ClangTidyForceLinker.h.
clangTidyAndroidModule
clangTidyAbseilModule
clangTidyAlteraModule
clangTidyBoostModule
clangTidyBugproneModule
clangTidyCERTModule
clangTidyConcurrencyModule
clangTidyCppCoreGuidelinesModule
clangTidyDarwinModule
clangTidyFuchsiaModule
clangTidyGoogleModule
clangTidyHICPPModule
clangTidyLinuxKernelModule
clangTidyLLVMModule
clangTidyLLVMLibcModule
clangTidyMiscModule
clangTidyModernizeModule
clangTidyObjCModule
clangTidyOpenMPModule
clangTidyPerformanceModule
clangTidyPortabilityModule
clangTidyReadabilityModule
clangTidyZirconModule
)
function(setup_llvm LLVM_VERSION) function(setup_llvm LLVM_VERSION)
find_package(Python3 COMPONENTS Interpreter REQUIRED) find_package(Python3 COMPONENTS Interpreter REQUIRED)
@@ -54,22 +25,6 @@ function(setup_llvm LLVM_VERSION)
list(APPEND LLVM_SETUP_ARGS "--offline") list(APPEND LLVM_SETUP_ARGS "--offline")
endif() 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( execute_process(
COMMAND "${Python3_EXECUTABLE}" "${LLVM_SETUP_SCRIPT}" ${LLVM_SETUP_ARGS} COMMAND "${Python3_EXECUTABLE}" "${LLVM_SETUP_SCRIPT}" ${LLVM_SETUP_ARGS}
RESULT_VARIABLE LLVM_SETUP_RESULT RESULT_VARIABLE LLVM_SETUP_RESULT
@@ -116,6 +71,29 @@ function(setup_llvm LLVM_VERSION)
clangSerialization clangSerialization
clangTidy clangTidy
clangTidyUtils 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 clangTooling
clangToolingCore clangToolingCore
clangToolingInclusions clangToolingInclusions
@@ -123,15 +101,8 @@ function(setup_llvm LLVM_VERSION)
clangToolingSyntax clangToolingSyntax
) )
else() else()
file(GLOB LLVM_LIBRARIES CONFIGURE_DEPENDS "${LLVM_INSTALL_PATH}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}LLVM[a-zA-Z]*${CMAKE_STATIC_LIBRARY_SUFFIX}") file(GLOB LLVM_LIBRARIES CONFIGURE_DEPENDS "${LLVM_INSTALL_PATH}/lib/*${CMAKE_STATIC_LIBRARY_SUFFIX}")
file(GLOB CLANG_LIBRARIES CONFIGURE_DEPENDS "${LLVM_INSTALL_PATH}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}clang[a-zA-Z]*${CMAKE_STATIC_LIBRARY_SUFFIX}") target_link_libraries(llvm-libs INTERFACE ${LLVM_LIBRARIES})
# TODO: find a better way to find out whether zlib and zstd are needed
# Currently link if present in the LLVM lib directory
file(GLOB OTHER_REQUIRED_LIBS CONFIGURE_DEPENDS
"${LLVM_INSTALL_PATH}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}z${CMAKE_STATIC_LIBRARY_SUFFIX}"
"${LLVM_INSTALL_PATH}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}zstd${CMAKE_STATIC_LIBRARY_SUFFIX}"
)
target_link_libraries(llvm-libs INTERFACE ${LLVM_LIBRARIES} ${CLANG_LIBRARIES} ${OTHER_REQUIRED_LIBS})
target_compile_definitions(llvm-libs INTERFACE CLANG_BUILD_STATIC=1) target_compile_definitions(llvm-libs INTERFACE CLANG_BUILD_STATIC=1)
endif() endif()
endfunction() endfunction()

View File

@@ -1,12 +1,34 @@
include_guard() include_guard()
include(${CMAKE_CURRENT_LIST_DIR}/llvm.cmake) include(${CMAKE_CURRENT_LIST_DIR}/llvm.cmake)
setup_llvm("21.1.8") setup_llvm("21.1.4+r1")
# install dependencies # install dependencies
include(FetchContent) include(FetchContent)
set(FETCHCONTENT_UPDATES_DISCONNECTED ON) set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
if(WIN32)
set(NULL_DEVICE NUL)
else()
set(NULL_DEVICE /dev/null)
endif()
# libuv
FetchContent_Declare(
libuv
GIT_REPOSITORY https://github.com/libuv/libuv.git
GIT_TAG v1.x
GIT_SHALLOW TRUE
)
if(NOT WIN32 AND CMAKE_BUILD_TYPE STREQUAL "Debug")
set(ASAN ON CACHE BOOL "Enable AddressSanitizer for libuv" FORCE)
endif()
set(LIBUV_BUILD_SHARED OFF CACHE BOOL "" FORCE)
set(LIBUV_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
# spdlog # spdlog
FetchContent_Declare( FetchContent_Declare(
spdlog spdlog
@@ -14,8 +36,14 @@ FetchContent_Declare(
GIT_TAG v1.15.3 GIT_TAG v1.15.3
GIT_SHALLOW TRUE GIT_SHALLOW TRUE
) )
set(SPDLOG_USE_STD_FORMAT ON CACHE BOOL "" FORCE)
set(SPDLOG_NO_EXCEPTIONS ON CACHE BOOL "" FORCE) # tomlplusplus
FetchContent_Declare(
tomlplusplus
GIT_REPOSITORY https://github.com/marzer/tomlplusplus.git
GIT_TAG v3.4.0
GIT_SHALLOW TRUE
)
# croaring # croaring
FetchContent_Declare( FetchContent_Declare(
@@ -38,18 +66,36 @@ set(FLATBUFFERS_BUILD_GRPC OFF CACHE BOOL "" FORCE)
set(FLATBUFFERS_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(FLATBUFFERS_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(FLATBUFFERS_BUILD_FLATHASH OFF CACHE BOOL "" FORCE) set(FLATBUFFERS_BUILD_FLATHASH OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(libuv spdlog tomlplusplus croaring flatbuffers)
if(CLICE_ENABLE_TEST)
# cpptrace
FetchContent_Declare( FetchContent_Declare(
kotatsu cpptrace
GIT_REPOSITORY https://github.com/clice-io/kotatsu GIT_REPOSITORY https://github.com/jeremy-rifkin/cpptrace.git
GIT_TAG 73814044ce8142f4438a3028f44668675fc09fff GIT_TAG v1.0.4
GIT_SHALLOW TRUE
) )
set(CPPTRACE_DISABLE_CXX_20_MODULES ON CACHE BOOL "" FORCE)
set(KOTA_ENABLE_ZEST ON) FetchContent_MakeAvailable(cpptrace)
set(KOTA_ENABLE_TEST OFF) endif()
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(kotatsu spdlog croaring flatbuffers) if(WIN32)
target_compile_definitions(uv_a PRIVATE _CRT_SECURE_NO_WARNINGS)
endif()
if(NOT MSVC AND TARGET uv_a)
target_compile_options(uv_a PRIVATE
"-Wno-unused-function"
"-Wno-unused-variable"
"-Wno-unused-but-set-variable"
"-Wno-deprecated-declarations"
"-Wno-missing-braces"
)
endif()
target_compile_definitions(spdlog PUBLIC
SPDLOG_USE_STD_FORMAT=1
SPDLOG_NO_EXCEPTIONS=1
)

View File

@@ -1,78 +0,0 @@
include_guard()
set(CLICE_PACK_DIR "${PROJECT_BINARY_DIR}/pack")
set(CLICE_SYMBOL_DIR "${PROJECT_BINARY_DIR}/pack-symbol")
if(WIN32)
set(CLICE_ARCHIVE_EXT ".zip")
set(CLICE_SYMBOL_NAME "clice.pdb")
else()
set(CLICE_ARCHIVE_EXT ".tar.gz")
if(APPLE)
set(CLICE_SYMBOL_NAME "clice.dSYM")
else()
set(CLICE_SYMBOL_NAME "clice.debug")
endif()
endif()
if(WIN32)
add_custom_target(clice-strip ALL
COMMAND ${CMAKE_COMMAND} -E make_directory "${CLICE_SYMBOL_DIR}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_PDB_FILE:clice>"
"${CLICE_SYMBOL_DIR}/${CLICE_SYMBOL_NAME}"
DEPENDS clice
COMMENT "Collecting PDB for clice"
)
elseif(APPLE)
add_custom_target(clice-strip ALL
COMMAND ${CMAKE_COMMAND} -E make_directory "${CLICE_SYMBOL_DIR}"
COMMAND dsymutil "$<TARGET_FILE:clice>" -o "${CLICE_SYMBOL_DIR}/${CLICE_SYMBOL_NAME}"
COMMAND strip -x "$<TARGET_FILE:clice>"
DEPENDS clice
COMMENT "Extracting dSYM and stripping clice"
)
else()
add_custom_target(clice-strip ALL
COMMAND ${CMAKE_COMMAND} -E make_directory "${CLICE_SYMBOL_DIR}"
COMMAND ${CMAKE_OBJCOPY} --only-keep-debug "$<TARGET_FILE:clice>" "${CLICE_SYMBOL_DIR}/${CLICE_SYMBOL_NAME}"
COMMAND ${CMAKE_STRIP} --strip-debug --strip-unneeded "$<TARGET_FILE:clice>"
COMMAND ${CMAKE_OBJCOPY} --add-gnu-debuglink="${CLICE_SYMBOL_DIR}/${CLICE_SYMBOL_NAME}" "$<TARGET_FILE:clice>"
DEPENDS clice
COMMENT "Extracting debug symbols and stripping clice"
)
endif()
add_custom_target(clice-pack ALL
DEPENDS clice-strip copy_clang_resource
COMMAND ${CMAKE_COMMAND} -E rm -rf "${CLICE_PACK_DIR}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${CLICE_PACK_DIR}/clice/bin"
COMMAND ${CMAKE_COMMAND} -E copy "$<TARGET_FILE:clice>" "${CLICE_PACK_DIR}/clice/bin/"
COMMAND ${CMAKE_COMMAND} -E copy_directory "${LLVM_INSTALL_PATH}/lib/clang" "${CLICE_PACK_DIR}/clice/lib/clang"
COMMAND ${CMAKE_COMMAND} -E copy "${PROJECT_SOURCE_DIR}/docs/clice.toml" "${CLICE_PACK_DIR}/clice/"
COMMAND ${CMAKE_COMMAND}
-DOUTPUT="${PROJECT_BINARY_DIR}/clice${CLICE_ARCHIVE_EXT}"
-DWORK_DIR="${CLICE_PACK_DIR}"
-P "${PROJECT_SOURCE_DIR}/cmake/archive.cmake"
COMMENT "Packaging clice distribution"
)
if(APPLE)
set(CLICE_COPY_SYMBOL_CMD ${CMAKE_COMMAND} -E copy_directory
"${CLICE_SYMBOL_DIR}/${CLICE_SYMBOL_NAME}" "${CLICE_SYMBOL_DIR}/pack/${CLICE_SYMBOL_NAME}")
else()
set(CLICE_COPY_SYMBOL_CMD ${CMAKE_COMMAND} -E copy
"${CLICE_SYMBOL_DIR}/${CLICE_SYMBOL_NAME}" "${CLICE_SYMBOL_DIR}/pack/")
endif()
add_custom_target(clice-pack-symbol ALL
DEPENDS clice-strip
COMMAND ${CMAKE_COMMAND} -E rm -rf "${CLICE_SYMBOL_DIR}/pack"
COMMAND ${CMAKE_COMMAND} -E make_directory "${CLICE_SYMBOL_DIR}/pack"
COMMAND ${CLICE_COPY_SYMBOL_CMD}
COMMAND ${CMAKE_COMMAND}
-DOUTPUT="${PROJECT_BINARY_DIR}/clice-symbol${CLICE_ARCHIVE_EXT}"
-DWORK_DIR="${CLICE_SYMBOL_DIR}/pack"
-P "${PROJECT_SOURCE_DIR}/cmake/archive.cmake"
COMMENT "Packaging clice debug symbols"
)

View File

@@ -1,73 +1,44 @@
cmake_minimum_required(VERSION 3.30) cmake_minimum_required(VERSION 3.30)
# Cross-compilation support via CLICE_TARGET_TRIPLE. if(WIN32)
# Examples: set(CMAKE_C_COMPILER clang-cl CACHE STRING "C compiler")
# -DCLICE_TARGET_TRIPLE=x86_64-apple-darwin (macOS x64 from arm64) set(CMAKE_CXX_COMPILER clang-cl CACHE STRING "C++ compiler")
# -DCLICE_TARGET_TRIPLE=aarch64-linux-gnu (Linux arm64 from x64) set(AR_PROGRAM_NAME "llvm-lib")
# -DCLICE_TARGET_TRIPLE=aarch64-pc-windows-msvc (Windows arm64 from x64) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL" CACHE STRING "MSVC runtime")
if(DEFINED CLICE_TARGET_TRIPLE)
if(CLICE_TARGET_TRIPLE MATCHES "^x86_64-apple-darwin") find_program(LLVM_LLD_LINK_PATH "lld-link")
set(CMAKE_OSX_ARCHITECTURES "x86_64" CACHE STRING "") if(LLVM_LLD_LINK_PATH)
elseif(CLICE_TARGET_TRIPLE MATCHES "^aarch64-.*linux") set(CMAKE_LINKER "${LLVM_LLD_LINK_PATH}" CACHE FILEPATH "Linker")
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()
else()
set(CMAKE_C_COMPILER clang CACHE STRING "C compiler")
set(CMAKE_CXX_COMPILER clang++ CACHE STRING "C++ compiler")
set(AR_PROGRAM_NAME "llvm-ar")
set(CMAKE_EXE_LINKER_FLAGS "-fuse-ld=lld" CACHE STRING "Executable linker flags")
set(CMAKE_SHARED_LINKER_FLAGS "-fuse-ld=lld" CACHE STRING "Shared library linker flags")
set(CMAKE_MODULE_LINKER_FLAGS "-fuse-ld=lld" CACHE STRING "Module linker flags")
endif() endif()
set(CMAKE_C_COMPILER clang CACHE STRING "") find_program(LLVM_AR_PATH ${AR_PROGRAM_NAME})
set(CMAKE_CXX_COMPILER clang++ CACHE STRING "")
find_program(LLVM_AR_PATH "llvm-ar")
if(LLVM_AR_PATH) if(LLVM_AR_PATH)
set(CMAKE_AR "${LLVM_AR_PATH}" CACHE FILEPATH "") set(CMAKE_AR "${LLVM_AR_PATH}" CACHE FILEPATH "Archiver")
set(CMAKE_C_COMPILER_AR "${LLVM_AR_PATH}" CACHE FILEPATH "") set(CMAKE_C_COMPILER_AR "${LLVM_AR_PATH}" CACHE FILEPATH "C archiver")
set(CMAKE_CXX_COMPILER_AR "${LLVM_AR_PATH}" CACHE FILEPATH "") set(CMAKE_CXX_COMPILER_AR "${LLVM_AR_PATH}" CACHE FILEPATH "C++ archiver")
endif() endif()
find_program(LLVM_RANLIB_PATH "llvm-ranlib") find_program(LLVM_RANLIB_PATH "llvm-ranlib")
if(LLVM_RANLIB_PATH) if(LLVM_RANLIB_PATH)
set(CMAKE_RANLIB "${LLVM_RANLIB_PATH}" CACHE FILEPATH "") set(CMAKE_RANLIB "${LLVM_RANLIB_PATH}" CACHE FILEPATH "Ranlib")
set(CMAKE_C_COMPILER_RANLIB "${LLVM_RANLIB_PATH}" CACHE FILEPATH "") set(CMAKE_C_COMPILER_RANLIB "${LLVM_RANLIB_PATH}" CACHE FILEPATH "C ranlib")
set(CMAKE_CXX_COMPILER_RANLIB "${LLVM_RANLIB_PATH}" CACHE FILEPATH "") set(CMAKE_CXX_COMPILER_RANLIB "${LLVM_RANLIB_PATH}" CACHE FILEPATH "C++ ranlib")
endif() endif()
find_program(LLVM_NM_PATH "llvm-nm") find_program(LLVM_NM_PATH "llvm-nm")
if(LLVM_NM_PATH) if(LLVM_NM_PATH)
set(CMAKE_NM "${LLVM_NM_PATH}" CACHE FILEPATH "") set(CMAKE_NM "${LLVM_NM_PATH}" CACHE FILEPATH "Symbol lister")
endif() endif()
find_program(LLVM_RC_PATH "llvm-rc") find_program(LLVM_RC_PATH "llvm-rc")
if(LLVM_RC_PATH) if(LLVM_RC_PATH)
set(CMAKE_RC_COMPILER "${LLVM_RC_PATH}" CACHE FILEPATH "") set(CMAKE_RC_COMPILER "${LLVM_RC_PATH}" CACHE FILEPATH "Resource compiler")
endif()
if(WIN32)
find_program(SCCACHE_PATH "sccache")
if(SCCACHE_PATH)
set(CMAKE_C_COMPILER_LAUNCHER "${SCCACHE_PATH}" CACHE FILEPATH "")
set(CMAKE_CXX_COMPILER_LAUNCHER "${SCCACHE_PATH}" CACHE FILEPATH "")
endif()
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL" CACHE STRING "")
set(CMAKE_EXE_LINKER_FLAGS_INIT "-fuse-ld=lld-link")
set(CMAKE_SHARED_LINKER_FLAGS_INIT "-fuse-ld=lld-link")
set(CMAKE_MODULE_LINKER_FLAGS_INIT "-fuse-ld=lld-link")
else()
find_program(CCACHE_PATH "ccache")
if(CCACHE_PATH)
set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PATH}" CACHE FILEPATH "")
set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PATH}" CACHE FILEPATH "")
endif()
set(CMAKE_EXE_LINKER_FLAGS_INIT "-fuse-ld=lld")
set(CMAKE_SHARED_LINKER_FLAGS_INIT "-fuse-ld=lld")
set(CMAKE_MODULE_LINKER_FLAGS_INIT "-fuse-ld=lld")
endif() endif()

View File

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

View File

@@ -6,7 +6,7 @@ export default defineConfig({
title: "clice", title: "clice",
description: "a powerful and modern C++ language server", description: "a powerful and modern C++ language server",
cleanUrls: true, cleanUrls: true,
base: "/clice/", base: "/",
rewrites: { rewrites: {
"en/:rest*": ":rest*", "en/:rest*": ":rest*",
}, },

View File

@@ -20,7 +20,7 @@ max_active_file = 8
cache_dir = "${workspace}/.clice/cache" cache_dir = "${workspace}/.clice/cache"
# Directory for storing index files. # Directory for storing index files.
index_dir = "${workspace}/.clice/index" index_dir = "${workspace}/.clice/index"
logging_dir = "${workspace}/.clice/logs" logging_dir = "${workspace}/.clice/logging"
# Compile commands files or directories to search for compile_commands.json files. # Compile commands files or directories to search for compile_commands.json files.
compile_commands_paths = ["${workspace}/build"] compile_commands_paths = ["${workspace}/build"]

View File

@@ -1,183 +0,0 @@
# Server Architecture
clice uses a **multi-process architecture** where a single **Master Server** coordinates multiple **Worker** processes. This design isolates Clang AST operations (which are memory-heavy and may crash) from the main LSP event loop.
## Overview
```
┌──────────────┐ JSON/LSP ┌────────────────┐ Bincode/IPC ┌──────────────────┐
│ LSP Client │ ◄──────────► │ Master Server │ ◄─────────────► │ Stateful Workers │
│ (Editor) │ (stdio) │ │ (stdio) │ (AST cache) │
└──────────────┘ │ - Lifecycle │ └──────────────────┘
│ - Documents │
│ - CDB │ Bincode/IPC ┌──────────────────┐
│ - Build drain │ ◄─────────────► │ Stateless Workers│
│ - Indexing │ (stdio) │ (one-shot tasks)│
└────────────────┘ └──────────────────┘
```
## Master Server
The master server (`src/server/master_server.cpp`) is the central coordinator. It runs a single-threaded async event loop and never touches Clang directly. Its responsibilities:
### LSP Lifecycle
The server progresses through these states:
1. **Uninitialized** — waiting for `initialize` request
2. **Initialized** — capabilities exchanged, waiting for `initialized` notification
3. **Ready** — workers spawned, workspace loaded, accepting requests
4. **ShuttingDown**`shutdown` received, draining work
5. **Exited**`exit` received, stopping the event loop
On `initialized`, the master:
- Loads configuration from `clice.toml` (or uses defaults)
- Starts the worker pool (spawns stateful + stateless processes)
- Loads `compile_commands.json` and builds an include graph
- Starts the background indexer coroutine (if enabled)
### Document Management
Each open document is tracked in a `DocumentState` with:
- Current `version` and `text` (kept in sync via `didOpen`/`didChange`)
- A `generation` counter to detect stale compile results
- Build state flags (`build_running`, `build_requested`, `drain_scheduled`)
When a document is opened or changed:
1. The include graph is re-scanned (via dependency directives)
2. The compile unit is registered/updated in the `CompileGraph`
3. A debounced build is scheduled
### Build Drain
The `run_build_drain` coroutine implements debounced compilation:
1. Wait for the debounce timer (default 200ms) to expire
2. Ensure PCH/PCM dependencies are ready via `CompileGraph`
3. Send a `compile` request to the assigned stateful worker
4. Publish diagnostics from the result (or clear them on failure)
5. If more edits arrived during compilation (`build_requested`), loop back to step 2
This ensures rapid typing doesn't trigger a compile per keystroke.
### Request Routing
Feature requests are split between two worker types:
**Stateful workers** (affinity-routed by file path):
- `textDocument/hover`
- `textDocument/semanticTokens/full`
- `textDocument/inlayHint`
- `textDocument/foldingRange`
- `textDocument/documentSymbol`
- `textDocument/documentLink`
- `textDocument/codeAction`
- `textDocument/definition`
**Stateless workers** (round-robin):
- `textDocument/completion`
- `textDocument/signatureHelp`
All feature responses use `RawValue` passthrough — the worker serializes the LSP result to JSON, and the master forwards the raw JSON bytes to the client without deserializing. This avoids bincode↔JSON conversion overhead and serde annotation conflicts.
## Worker Pool
The worker pool (`src/server/worker_pool.cpp`) manages spawning and communicating with worker processes. Each worker is a child process of the same `clice` binary, launched with `--mode stateful-worker` or `--mode stateless-worker`.
### Communication
Workers communicate with the master via **stdio pipes** using a **bincode** serialization format (via `kota::ipc::BincodePeer`). This is more compact and faster than JSON for internal IPC, while the master handles JSON for the external LSP protocol.
### Stateful Worker Routing
Stateful workers use **affinity routing**: each file is consistently assigned to the same worker so that the worker retains the cached AST. Assignment uses a **least-loaded** strategy for new files, with **LRU tracking** to manage ownership.
When a worker exceeds its document capacity (currently hardcoded at 16 documents), it evicts the least-recently-used document and notifies the master via an `evicted` notification.
### Stateless Worker Routing
Stateless workers use simple **round-robin** dispatch. Each request includes the full source text and compilation arguments, so any worker can handle it independently.
## Stateful Worker
The stateful worker (`src/server/stateful_worker.cpp`) caches compiled ASTs in memory. Key behavior:
- **Compile**: Parses source code into a `CompilationUnit`, caches the AST, and returns diagnostics as a `RawValue` (JSON bytes)
- **Feature queries**: Look up the cached AST and invoke the corresponding `feature::*` function (hover, semantic tokens, etc.), serializing the result to JSON
- **Document updates**: Received as notifications — the worker updates the stored text and marks the document as `dirty`, causing feature queries to return `null` until recompilation
- **Eviction**: LRU-based; evicts the oldest document when capacity is exceeded, notifying the master
- **Concurrency**: Each document has a per-document `kota::mutex` (strand) to serialize compilation and feature queries. Heavy work (compilation, feature extraction) runs on a thread pool via `kota::queue`.
## Stateless Worker
The stateless worker (`src/server/stateless_worker.cpp`) handles one-shot requests that don't benefit from cached ASTs:
- **Completion**: Creates a fresh compilation with `CompilationKind::Completion` and invokes `feature::code_complete`
- **Signature help**: Similar to completion, using `feature::signature_help`
- **Build PCH**: Compiles a precompiled header to a temporary file
- **Build PCM**: Compiles a C++20 module interface to a temporary file
- **Index**: Compiles a file for indexing (TUIndex generation — currently a stub)
All requests are dispatched to a thread pool via `kota::queue`.
## Compile Graph
The compile graph (`src/server/compile_graph.cpp`) tracks compilation unit dependencies as a DAG. It handles:
- **Registration**: Each file registers its included dependencies
- **Cascade invalidation**: When a file changes, all transitive dependents are marked dirty and their ongoing compilations are cancelled
- **Dependency compilation**: Before compiling a file, `compile_deps` ensures all dependencies (PCH, PCMs) are built first
- **Cancellation**: Uses `kota::cancellation_source` to abort in-flight compilations when files are invalidated
## Configuration
The server reads configuration from `clice.toml` (or `.clice/config.toml`) in the workspace root. If no config file exists, sensible defaults are computed from system resources:
| Setting | Default | Description |
| ------------------------ | --------------------- | ------------------------------------------- |
| `stateful_worker_count` | CPU cores / 4 | Number of stateful worker processes |
| `stateless_worker_count` | CPU cores / 4 | Number of stateless worker processes |
| `worker_memory_limit` | 4 GB | Memory limit per stateful worker |
| `compile_commands_path` | auto-detect | Path to `compile_commands.json` |
| `cache_dir` | `<workspace>/.clice/` | Cache directory for PCH/PCM files |
| `debounce_ms` | 200 | Debounce interval for recompilation |
| `enable_indexing` | true | Enable background indexing |
| `idle_timeout_ms` | 3000 | Idle time before background indexing starts |
String values support `${workspace}` substitution.
## IPC Protocol
The master and workers communicate using custom RPC messages defined in `src/server/protocol/`. Each message type has a `RequestTraits` or `NotificationTraits` specialization that defines the method name and result type.
### Stateful Worker Messages
| Method | Direction | Purpose |
| ----------------------------- | ------------ | ------------------------------------- |
| `clice/worker/compile` | Request | Compile source and return diagnostics |
| `clice/worker/hover` | Request | Get hover info at position |
| `clice/worker/semanticTokens` | Request | Get semantic tokens for file |
| `clice/worker/inlayHints` | Request | Get inlay hints for range |
| `clice/worker/foldingRange` | Request | Get folding ranges |
| `clice/worker/documentSymbol` | Request | Get document symbols |
| `clice/worker/documentLink` | Request | Get document links |
| `clice/worker/codeAction` | Request | Get code actions for range |
| `clice/worker/goToDefinition` | Request | Go to definition at position |
| `clice/worker/documentUpdate` | Notification | Update document text (marks dirty) |
| `clice/worker/evict` | Notification | Master → Worker: evict a document |
| `clice/worker/evicted` | Notification | Worker → Master: document was evicted |
### Stateless Worker Messages
| Method | Direction | Purpose |
| ---------------------------- | --------- | ---------------------------- |
| `clice/worker/completion` | Request | Code completion at position |
| `clice/worker/signatureHelp` | Request | Signature help at position |
| `clice/worker/buildPCH` | Request | Build precompiled header |
| `clice/worker/buildPCM` | Request | Build C++20 module interface |
| `clice/worker/index` | Request | Index a translation unit |

View File

@@ -32,6 +32,18 @@ pixi run integration-test Debug
> [!TIP] > [!TIP]
> If you want to develop directly with `cmake`, `ninja`, `clang++`, etc., run `pixi shell` to enter a shell with all env vars configured. > If you want to develop directly with `cmake`, `ninja`, `clang++`, etc., run `pixi shell` to enter a shell with all env vars configured.
### XMake
We also support building with XMake:
```shell
# config & build (default releasedbg)
pixi run xmake
# unit & integration
pixi run xmake-test
```
## Manual Build ## Manual Build
If you plan to build manually, first ensure your toolchain matches the versions defined in `pixi.toml`. If you plan to build manually, first ensure your toolchain matches the versions defined in `pixi.toml`.
@@ -57,6 +69,24 @@ Optional build options:
| CLICE_ENABLE_TEST | OFF | Build clice unit tests | | CLICE_ENABLE_TEST | OFF | Build clice unit tests |
| CLICE_USE_LIBCXX | OFF | Build clice with libc++ (adds `-std=libc++`); if enabled, ensure the LLVM libs are also built with libc++ | | CLICE_USE_LIBCXX | OFF | Build clice with libc++ (adds `-std=libc++`); if enabled, ensure the LLVM libs are also built with libc++ |
| CLICE_CI_ENVIRONMENT | OFF | Enable the `CLICE_CI_ENVIRONMENT` macro; some tests only run in CI | | CLICE_CI_ENVIRONMENT | OFF | Enable the `CLICE_CI_ENVIRONMENT` macro; some tests only run in CI |
| CLICE_BUILTIN_LIBRARY_MODULES | "" | Semicolon-separated list of CMake modules that register builtin libraries compiled into `clice`; see [Builtin Libraries](./builtin-library.md) |
### XMake
Build clice with:
```bash
xmake f -c --mode=releasedbg --toolchain=clang
xmake build --all
```
Optional build options:
| Option | Default | Effect |
| ------------- | ------- | ---------------------------------------- |
| --llvm | "" | Build clice with LLVM from a custom path |
| --enable_test | false | Build clice unit tests |
| --ci | false | Enable `CLICE_CI_ENVIRONMENT` |
## About LLVM ## About LLVM
@@ -64,7 +94,7 @@ clice calls Clang APIs to parse C++ code, so it must link against LLVM/Clang. Be
Two ways to satisfy this dependency: Two ways to satisfy this dependency:
1. We publish prebuilt binaries of the LLVM version we use at [clice-llvm](https://github.com/clice-io/clice-llvm/releases) for CI and release builds. During builds, cmake downloads these LLVM libs by default. 1. We publish prebuilt binaries of the LLVM version we use at [clice-llvm](https://github.com/clice-io/clice-llvm/releases) for CI and release builds. During builds, cmake and xmake download these LLVM libs by default.
> [!IMPORTANT] > [!IMPORTANT]
> >

View File

@@ -0,0 +1,101 @@
# Builtin Libraries
Builtin libraries are compiled directly into the `clice` binary instead of being loaded later with `--plugin-path`.
This is useful when:
- your plugin sources live outside the `clice` source tree
- you want the builtin to be part of the default executable
- you need extra include directories, compile definitions, or link dependencies during the main build
## CMake Entry Point
`clice` now exposes a small helper module at [cmake/builtin-libraries.cmake](/cmake/builtin-libraries.cmake).
Extra builtin libraries are registered through the cache variable `CLICE_BUILTIN_LIBRARY_MODULES`.
Each value in `CLICE_BUILTIN_LIBRARY_MODULES` must be a CMake file. During configure, `clice` includes those files, and each file calls `clice_add_builtin_library(...)`.
## Minimal Module
Create a CMake file in your external project, for example `/path/to/my-plugin/clice-builtin.cmake`:
```cmake
clice_add_builtin_library(
NAME my_plugin
SOURCES
"${CMAKE_CURRENT_LIST_DIR}/src/MyPlugin.cpp"
INCLUDE_DIRECTORIES
"${CMAKE_CURRENT_LIST_DIR}/include"
ENTRYPOINT
clice_get_my_plugin_server_plugin_info
)
```
Then configure `clice` with:
```shell
cmake -B build -G Ninja \
-DCLICE_BUILTIN_LIBRARY_MODULES="/path/to/my-plugin/clice-builtin.cmake"
```
To load multiple modules, pass a semicolon-separated CMake list:
```shell
cmake -B build -G Ninja \
-DCLICE_BUILTIN_LIBRARY_MODULES="/path/to/a.cmake;/path/to/b.cmake"
```
## `clice_add_builtin_library`
The helper accepts the following arguments:
| Argument | Required | Description |
| --- | --- | --- |
| `NAME` | Yes | Logical name used to create an internal object target |
| `SOURCES` | Yes | Source files compiled into `clice`; absolute paths and out-of-tree paths are supported |
| `ENTRYPOINT` | Yes | Unique function name in namespace `clice` that returns `::clice::PluginInfo` |
| `INCLUDE_DIRECTORIES` | No | Extra include directories for this builtin only |
| `LINK_LIBRARIES` | No | Extra libraries or targets needed by this builtin |
| `COMPILE_DEFINITIONS` | No | Extra compile definitions for this builtin |
| `COMPILE_OPTIONS` | No | Extra compile options for this builtin |
## Entrypoint Requirements
Builtin libraries share a single final executable, so each builtin must use its own unique entrypoint function inside namespace `clice`.
Dynamic plugins use:
```cpp
clice_get_server_plugin_info()
```
Builtin libraries should use a unique name such as:
```cpp
clice_get_my_plugin_server_plugin_info()
```
For example:
```cpp
#include "Server/Plugin.h"
namespace clice {
::clice::PluginInfo clice_get_my_plugin_server_plugin_info() {
return {
CLICE_PLUGIN_API_VERSION,
"MyPlugin",
"v0.0.1",
CLICE_PLUGIN_DEF_HASH,
[](clice::ServerPluginBuilder& builder) {
// register callbacks here
},
};
}
} // namespace clice
```
`clice` generates the static registration glue automatically, so once the module is included, no additional edits to `src/clice.cc` are required.

View File

@@ -0,0 +1,78 @@
You can implement a clice server plugin to extend clice's functionality.
## Use case
When you use `clice` as LSP backend of LLM agents, e.g. claude code, you can add plugin to provide some extra features.
## Writing a plugin
When a plugin is loaded by the server, it will call `clice_get_server_plugin_info` to obtain information about this plugin and about how to register its customization points.
This function needs to be implemented by the plugin, see the example below:
```c++
extern "C" ::clice::PluginInfo LLVM_ATTRIBUTE_WEAK
clice_get_server_plugin_info() {
return {
CLICE_PLUGIN_API_VERSION, "MyPlugin", "v0.1", CLICE_PLUGIN_DEF_HASH,
[](ServerPluginBuilder builder) { ... }
};
}
```
See [PluginProtocol.h](/include/Server/PluginProtocol.h) for more details.
## Compiling a plugin
The plugin must be compiled with the same dependencies and compiler options as clice, otherwise it will cause undefined behavior. [config/llvm-manifest.json](/config/llvm-manifest.json) defines the build information used by clice.
## Loading plugins
For security reasons, clice does not allow loading plugins through configuration files, but must specify the plugin path through command line options.
When `clice` starts, it will load all plugins specified in the command line. You can specify the plugin path through the `--plugin-path` option.
```shell
$ clice --plugin-path /path/to/my-plugin.so
```
## Getting content of `CLICE_PLUGIN_DEF_HASH`
There are two values to return in the `clice_get_server_plugin_info` function.
- `CLICE_PLUGIN_API_VERSION` is used to ensure compability of the `clice_get_server_plugin_info` function between the plugin and the server.
- `CLICE_PLUGIN_DEF_HASH` is used to ensure the consistency of the C++ declarations between the plugin and the server.
To debug the content of `CLICE_PLUGIN_DEF_HASH`, you can run following command:
```shell
$ git clone https://github.com/clice-io/clice.git
$ cd clice
$ git checkout `clice --version --git-describe`
$ python scripts/plugin-def.py content
```
You will get a C source code file, content of which is like this:
```cpp
#if 0
// begin of config/llvm-manifest.json
[
{
"version": "21.1.4+r1",
"filename": "arm64-macos-clang-debug-asan.tar.xz",
"sha256": "7da4b7d63edefecaf11773e7e701c575140d1a07329bbbb038673b6ee4516ff5",
"lto": false,
"asan": true,
"platform": "macosx",
"build_type": "Debug"
},
...
]
...
#endif
```
## Builtin libraries
If you want to compile a plugin directly into the `clice` binary instead of loading it dynamically, see [Builtin Libraries](./builtin-library.md).

View File

@@ -18,6 +18,13 @@ We use pytest to run integration tests. Please refer to `pyproject.toml` to inst
$ pytest -s --log-cli-level=INFO tests/integration --executable=./build/bin/clice $ pytest -s --log-cli-level=INFO tests/integration --executable=./build/bin/clice
``` ```
If you use xmake as your build system, you can run the tests directly with xmake:
```shell
$ xmake run --verbose unit_tests
$ xmake test --verbose integration_tests/default
```
## Debug ## Debug
If you want to attach a debugger to clice for debugging, it is recommended to first start clice in socket mode independently, and then connect the client to it. If you want to attach a debugger to clice for debugging, it is recommended to first start clice in socket mode independently, and then connect the client to it.

View File

@@ -54,73 +54,14 @@ bazel run @hedron_compile_commands//:refresh_all
### Visual Studio ### Visual Studio
Visual Studio (2019 16.1+) can generate a compilation database via CMake integration. Open your project as a CMake project, then configure the generation in `CMakeSettings.json`: TODO:
```json
{
"configurations": [
{
"name": "x64-Debug",
"generator": "Ninja",
"buildRoot": "${projectDir}\\build",
"cmakeCommandArgs": "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON"
}
]
}
```
Alternatively, for MSBuild-based projects (`.vcxproj`), you can use [compiledb-vs](https://github.com/pjbroad/compiledb-vs) or [catter](https://github.com/clice-io/catter) to generate the compilation database.
### Makefile ### Makefile
For Makefile-based projects, use [bear](https://github.com/rizsotto/Bear) to intercept compilation commands: TODO:
```bash
bear -- make
```
This will generate a `compile_commands.json` in the current directory. Note that `bear` requires a clean build to capture all commands — run `make clean` before `bear -- make` if needed.
Alternatively, if you use GNU Make, you can use [compiledb](https://github.com/nicktimko/compiledb):
```bash
compiledb make
```
### Meson
Meson generates a compilation database automatically during setup:
```bash
meson setup build
```
The `compile_commands.json` will be in the `build` directory.
### Xmake ### Xmake
Use one of the following approaches to generate a compilation database.
#### Command Line
Run the following command to manually generate a compilation database:
```bash
xmake project -k compile_commands --lsp=clangd build
```
> Compilation database generated manually doesn't automatically update itself. Re-generate if changes are made to the project.
#### VSCode Extension
The Xmake official VSCode extension automatically generates the compilation database when `xmake.lua` is updated. However, it generates the database to the `.vscode` directory by default. Add this setting in `settings.json`:
```json
"xmake.compileCommandsDirectory": "build"
```
to explicitly ask the extension to generate the compilation database in `build`.
### Others ### Others
For any other build system, you can use [catter](https://github.com/clice-io/catter) to generate a compilation database. It captures compilation commands through a fake compiler approach and is designed to work reliably with any build system that invokes a compiler executable. For any other build system, you can try using [bear](https://github.com/rizsotto/Bear) or [scan-build](https://github.com/rizsotto/scan-build) to intercept compilation commands and obtain the compilation database (no guarantee of success). We plan to write a **new tool** in the future that captures compilation commands through a fake compiler approach.

View File

@@ -32,6 +32,18 @@ pixi run integration-test Debug
> [!TIP] > [!TIP]
> 如果你想直接使用 `cmake`, `ninja`, `clang++` 等命令进行开发,请运行 `pixi shell` 进入已配置好环境变量的终端 > 如果你想直接使用 `cmake`, `ninja`, `clang++` 等命令进行开发,请运行 `pixi shell` 进入已配置好环境变量的终端
### XMake
我们同样支持使用 XMake 构建:
```shell
# config & build (default releasedbg)
pixi run xmake
# unit & integration
pixi run xmake-test
```
## Manual Build ## Manual Build
如果你打算手动构建,请务必先确认你的工具链满足 pixi.toml 中定义的版本要求。 如果你打算手动构建,请务必先确认你的工具链满足 pixi.toml 中定义的版本要求。
@@ -57,6 +69,24 @@ cmake -B build -G Ninja \
| CLICE_ENABLE_TEST | OFF | 是否构建 clice 的单元测试 | | CLICE_ENABLE_TEST | OFF | 是否构建 clice 的单元测试 |
| CLICE_USE_LIBCXX | OFF | 是否使用 libc++ 来构建 clice添加 `-std=libc++`),如果开启,请确保 LLVM 库也是使用 libc++ 编译的 | | CLICE_USE_LIBCXX | OFF | 是否使用 libc++ 来构建 clice添加 `-std=libc++`),如果开启,请确保 LLVM 库也是使用 libc++ 编译的 |
| CLICE_CI_ENVIRONMENT | OFF | 是否打开 `CLICE_CI_ENVIRONMENT` 这个宏,有些测试在 CI 环境才会执行 | | CLICE_CI_ENVIRONMENT | OFF | 是否打开 `CLICE_CI_ENVIRONMENT` 这个宏,有些测试在 CI 环境才会执行 |
| CLICE_BUILTIN_LIBRARY_MODULES | "" | 以分号分隔的 CMake 模块列表,用于注册会被编译进 `clice` 的 builtin library详见 [Builtin Libraries](./builtin-library.md) |
### XMake
使用如下命令即可构建 clice
```bash
xmake f -c --mode=releasedbg --toolchain=clang
xmake build --all
```
可选的构建选项:
| 选项 | 默认值 | 效果 |
| ------------- | ------ | ------------------------------------ |
| --llvm | "" | 使用自定义路径的 LLVM 库来构建 clice |
| --enable_test | false | 是否构建 clice 的单元测试 |
| --ci | false | 是否打开 `CLICE_CI_ENVIRONMENT` |
## About LLVM ## About LLVM
@@ -64,7 +94,7 @@ clice 调用 Clang API 来解析 C++ 代码,因此必须链接 LLVM/Clang 库
主要有两种方式解决这个依赖问题: 主要有两种方式解决这个依赖问题:
1. 我们在 [clice-llvm](https://github.com/clice-io/clice-llvm/releases) 上会发布使用的 LLVM 版本的预编译二进制,用于 CI 或者 release 构建。在构建时 cmake 默认会从此处下载 LLVM 库然后使用。 1. 我们在 [clice-llvm](https://github.com/clice-io/clice-llvm/releases) 上会发布使用的 LLVM 版本的预编译二进制,用于 CI 或者 release 构建。在构建时 cmake 和 xmake 默认会从此处下载 LLVM 库然后使用。
> [!IMPORTANT] > [!IMPORTANT]
> >

View File

@@ -0,0 +1,101 @@
# Builtin Libraries
Builtin library 会被直接编译进 `clice` 可执行文件,而不是在运行时通过 `--plugin-path` 动态加载。
这种方式适合以下场景:
- 插件源码位于 `clice` 源码树之外
- 希望该 builtin 默认随 `clice` 可执行文件一起发布
- 需要为该 builtin 单独补充 include 路径、编译宏或链接依赖
## CMake 入口
`clice` 现在提供了一个辅助模块:[cmake/builtin-libraries.cmake](/cmake/builtin-libraries.cmake)。
额外的 builtin library 通过缓存变量 `CLICE_BUILTIN_LIBRARY_MODULES` 注册。
`CLICE_BUILTIN_LIBRARY_MODULES` 中的每一项都必须是一个 CMake 文件。配置阶段 `clice``include()` 这些文件,而每个文件都需要调用 `clice_add_builtin_library(...)`
## 最小示例
你可以在外部项目中创建一个 CMake 文件,例如 `/path/to/my-plugin/clice-builtin.cmake`
```cmake
clice_add_builtin_library(
NAME my_plugin
SOURCES
"${CMAKE_CURRENT_LIST_DIR}/src/MyPlugin.cpp"
INCLUDE_DIRECTORIES
"${CMAKE_CURRENT_LIST_DIR}/include"
ENTRYPOINT
clice_get_my_plugin_server_plugin_info
)
```
然后在配置 `clice` 时传入:
```shell
cmake -B build -G Ninja \
-DCLICE_BUILTIN_LIBRARY_MODULES="/path/to/my-plugin/clice-builtin.cmake"
```
如果要加载多个模块,可以传入以分号分隔的 CMake 列表:
```shell
cmake -B build -G Ninja \
-DCLICE_BUILTIN_LIBRARY_MODULES="/path/to/a.cmake;/path/to/b.cmake"
```
## `clice_add_builtin_library`
该辅助函数支持以下参数:
| 参数 | 必填 | 说明 |
| --- | --- | --- |
| `NAME` | 是 | 逻辑名称,会用于创建内部 object target |
| `SOURCES` | 是 | 要编译进 `clice` 的源文件,支持绝对路径和源码树外路径 |
| `ENTRYPOINT` | 是 | `clice` 命名空间中的唯一函数名,返回值类型为 `::clice::PluginInfo` |
| `INCLUDE_DIRECTORIES` | 否 | 仅对当前 builtin 生效的额外头文件目录 |
| `LINK_LIBRARIES` | 否 | 当前 builtin 额外需要的库或 target |
| `COMPILE_DEFINITIONS` | 否 | 当前 builtin 额外需要的编译宏 |
| `COMPILE_OPTIONS` | 否 | 当前 builtin 额外需要的编译选项 |
## Entrypoint 要求
所有 builtin library 最终都会被链接进同一个可执行文件,因此每个 builtin 都必须在 `clice` 命名空间中使用唯一的入口函数名。
动态插件通常使用:
```cpp
clice_get_server_plugin_info()
```
builtin library 应该改用类似下面的唯一名字:
```cpp
clice_get_my_plugin_server_plugin_info()
```
例如:
```cpp
#include "Server/Plugin.h"
namespace clice {
::clice::PluginInfo clice_get_my_plugin_server_plugin_info() {
return {
CLICE_PLUGIN_API_VERSION,
"MyPlugin",
"v0.0.1",
CLICE_PLUGIN_DEF_HASH,
[](clice::ServerPluginBuilder& builder) {
// 在这里注册回调
},
};
}
} // namespace clice
```
`clice` 会自动生成静态注册代码,因此只要模块被包含进来,就不需要再手动修改 `src/clice.cc`

View File

@@ -30,7 +30,7 @@ pixi run publish-vscode
1. `pixi shell -e node` 1. `pixi shell -e node`
2.`editors/vscode` 下运行 `pnpm run watch`(增量构建) 2.`editors/vscode` 下运行 `pnpm run watch`(增量构建)
3. VSCode 中使用Run Extension/Launch Extension”调试配置或执行 `code --extensionDevelopmentPath=$(pwd)/editors/vscode` 3. VSCode 中使用Run Extension/Launch Extension” 调试配置,或执行 `code --extensionDevelopmentPath=$(pwd)/editors/vscode`
常用脚本(在 `pixi shell -e node` 下): 常用脚本(在 `pixi shell -e node` 下):

View File

@@ -0,0 +1,78 @@
你可以在 clice 中实现一个 server plugin 来扩展 clice 的功能。
## 用例
当你使用 `clice` 作为 LLM 代理的 LSP 后端时,比如 claude code你可以添加插件来提供一些额外功能。
## 编写插件
当一个插件被服务器加载时,它会调用 `clice_get_server_plugin_info` 来获取关于这个插件的信息以及如何注册它的定制点。
这个函数需要由插件实现,请参考下面的示例:
```cpp
extern "C" ::clice::PluginInfo LLVM_ATTRIBUTE_WEAK
clice_get_server_plugin_info() {
return {
CLICE_PLUGIN_API_VERSION, "MyPlugin", "v0.1", CLICE_PLUGIN_DEF_HASH,
[](ServerPluginBuilder builder) { ... }
};
}
```
请参考 [PluginProtocol.h](/include/Server/PluginProtocol.h) 了解更多细节。
## 编译插件
插件必须使用与 clice 一致的依赖和编译器选项来编译,否则会导致 undefined behavior。[config/llvm-manifest.json](/config/llvm-manifest.json) 中定义了 clice 使用的构建信息。
## 加载插件
为了安全考虑clice 不允许通过配置文件来加载插件,而必须通过命令行选项来指定插件的路径。
`clice` 启动时,它会加载所有在命令行中指定的插件。你可以通过 `--plugin-path` 选项来指定插件的路径。
```shell
$ clice --plugin-path /path/to/my-plugin.so
```
## 获取 `CLICE_PLUGIN_DEF_HASH` 的内容
`clice_get_server_plugin_info` 函数中需要返回两个值。
- `CLICE_PLUGIN_API_VERSION` 用于确保插件和服务器之间的 `clice_get_server_plugin_info` 函数的一致性。
- `CLICE_PLUGIN_DEF_HASH` 用于确保插件和服务器之间的 C++ 声明的一致性。
要调试 `CLICE_PLUGIN_DEF_HASH` 的内容,你可以运行以下命令:
```shell
$ git clone https://github.com/clice-io/clice.git
$ cd clice
$ git checkout `clice --version --git-describe`
$ python scripts/plugin-def.py content > /tmp/plugin-proto.h
```
你将会得到一个 C 源码格式的文件,内容大致如下:
```cpp
#if 0
// begin of config/llvm-manifest.json
[
{
"version": "21.1.4+r1",
"filename": "arm64-macos-clang-debug-asan.tar.xz",
"sha256": "7da4b7d63edefecaf11773e7e701c575140d1a07329bbbb038673b6ee4516ff5",
"lto": false,
"asan": true,
"platform": "macosx",
"build_type": "Debug"
},
...
]
...
#endif
```
## Builtin libraries
如果你希望把插件直接编译进 `clice` 可执行文件,而不是在运行时动态加载,请参考 [Builtin Libraries](./builtin-library.md)。

View File

@@ -18,6 +18,13 @@ $ ./build/bin/unit_tests --test-dir="./tests/data"
$ pytest -s --log-cli-level=INFO tests/integration --executable=./build/bin/clice $ pytest -s --log-cli-level=INFO tests/integration --executable=./build/bin/clice
``` ```
如果你使用 xmake 作为构建系统,可以直接通过 xmake 运行测试:
```shell
$ xmake run --verbose unit_tests
$ xmake test --verbose integration_tests/default
```
## Debug ## Debug
如果想在 clice 上附加调试器并进行调试,推荐先单独以 socket 模式启动 clice然后再将客户端连接到 clice 上 如果想在 clice 上附加调试器并进行调试,推荐先单独以 socket 模式启动 clice然后再将客户端连接到 clice 上

View File

@@ -54,73 +54,14 @@ bazel run @hedron_compile_commands//:refresh_all
### Visual Studio ### Visual Studio
Visual Studio2019 16.1+)可以通过 CMake 集成来生成编译数据库。将项目作为 CMake 项目打开,然后在 `CMakeSettings.json` 中配置: TODO:
```json
{
"configurations": [
{
"name": "x64-Debug",
"generator": "Ninja",
"buildRoot": "${projectDir}\\build",
"cmakeCommandArgs": "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON"
}
]
}
```
对于基于 MSBuild 的项目(`.vcxproj`),可以使用 [compiledb-vs](https://github.com/pjbroad/compiledb-vs) 或 [catter](https://github.com/clice-io/catter) 来生成编译数据库。
### Makefile ### Makefile
对于基于 Makefile 的项目,使用 [bear](https://github.com/rizsotto/Bear) 来拦截编译命令: TODO:
```bash
bear -- make
```
这会在当前目录生成 `compile_commands.json`。注意 `bear` 需要干净的构建来捕获所有命令——如果需要的话,在运行 `bear -- make` 之前先执行 `make clean`
另外,如果使用 GNU Make也可以使用 [compiledb](https://github.com/nicktimko/compiledb)
```bash
compiledb make
```
### Meson
Meson 在配置阶段会自动生成编译数据库:
```bash
meson setup build
```
`compile_commands.json` 会生成在 `build` 目录下。
### Xmake ### Xmake
用下列任意方法生成编译数据库。
#### 命令行手动生成
在命令行中执行以下命令:
```bash
xmake project -k compile_commands --lsp=clangd build
```
> 通过这种方法生成的编译数据库无法自动更新,需要在项目编译配置更改时手动重新生成。
#### VSCode 扩展
Xmake 提供的官方 VSCode 扩展会在 `xmake.lua` 更新时自动生成编译数据库。然而默认情况下,它将编译数据库生成到了 `.vscode` 文件夹。在 `settings.json` 中添加以下配置:
```json
"xmake.compileCommandsDirectory": "build"
```
以将编译数据库的生成目录调整到 `build`,供 clice 使用。
### Others ### Others
对于任意其它的构建系统,可以使用 [catter](https://github.com/clice-io/catter) 来生成编译数据库。它通过伪装编译器的方式来捕获编译命令,能够可靠地与任何调用编译器可执行文件的构建系统配合工作 对于任意其它的构建系统,可以尝试使用 [bear](https://github.com/rizsotto/Bear) 或者 [scan-build](https://github.com/rizsotto/scan-build) 来拦截编译命令并获取到编译数据库(不保证成功)。我们计划在未来编写一个**新的工具**,通过假编译器的方式来实现编译命令的捕获

View File

@@ -0,0 +1,9 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"dbaeumer.vscode-eslint",
"amodio.tsl-problem-matcher",
"ms-vscode.extension-test-runner"
]
}

17
editors/vscode/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,17 @@
// A launch configuration that compiles the extension and then opens it inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "${defaultBuildTask}"
}
]
}

13
editors/vscode/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,13 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
"out": false, // set this to true to hide the "out" folder with the compiled JS files
"dist": false // set this to true to hide the "dist" folder with the compiled JS files
},
"search.exclude": {
"out": true, // set this to false to include "out" folder in search results
"dist": true // set this to false to include "dist" folder in search results
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off"
}

37
editors/vscode/.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,37 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "watch",
"problemMatcher": "$ts-webpack-watch",
"isBackground": true,
"presentation": {
"reveal": "never",
"group": "watchers"
},
"group": {
"kind": "build",
"isDefault": true
}
},
{
"type": "npm",
"script": "watch-tests",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "never",
"group": "watchers"
},
"group": "build"
},
{
"label": "tasks: watch-tests",
"dependsOn": ["npm: watch", "npm: watch-tests"],
"problemMatcher": []
}
]
}

View File

@@ -62,12 +62,7 @@
} }
} }
}, },
"commands": [ "commands": [],
{
"command": "clice.restart",
"title": "Clice: Restart Language Server"
}
],
"semanticTokenTypes": [ "semanticTokenTypes": [
{ {
"id": "character", "id": "character",

View File

@@ -10,7 +10,7 @@ interface Setting {
export function getSetting(): Setting | undefined { export function getSetting(): Setting | undefined {
const setting = vscode.workspace.getConfiguration("clice"); const setting = vscode.workspace.getConfiguration("clice");
const executable = setting.get<string>("executable"); const executable = setting.get<string>("executable");
const mode = process.env.CLICE_MODE || setting.get<string>("mode"); const mode = setting.get<string>("mode");
if (mode !== "pipe" && mode !== "socket") { if (mode !== "pipe" && mode !== "socket") {
vscode.window.showErrorMessage(`Unexpected mode: ${mode}`); vscode.window.showErrorMessage(`Unexpected mode: ${mode}`);

View File

@@ -1,8 +1,7 @@
#pragma once #pragma once
#include <concepts> #include "AST/SourceCode.h"
#include "Compiler/CompilationUnit.h"
#include "compile/compilation_unit.h"
#include "clang/AST/RecursiveASTVisitor.h" #include "clang/AST/RecursiveASTVisitor.h"

View File

@@ -0,0 +1,63 @@
#pragma once
#include "Support/Enum.h"
namespace clice {
struct RelationKind : refl::Enum<RelationKind, true, uint32_t> {
enum Kind : uint32_t {
Invalid,
Declaration,
Definition,
Reference,
WeakReference,
// Write Relation.
Read,
Write,
Interface,
Implementation,
/// When target is a type definition of source, source is possible type or constructor.
TypeDefinition,
/// When target is a base class of source.
Base,
/// When target is a derived class of source.
Derived,
/// When target is a constructor of source.
Constructor,
/// When target is a destructor of source.
Destructor,
// When target is a caller of source.
Caller,
// When target is a callee of source.
Callee,
};
using Enum::Enum;
constexpr bool isDeclOrDef() {
return is_one_of(Declaration, Definition);
}
constexpr bool isReference() {
return is_one_of(Reference, WeakReference);
}
constexpr bool isBetweenSymbol() {
return is_one_of(Interface,
Implementation,
TypeDefinition,
Base,
Derived,
Constructor,
Destructor);
}
constexpr bool isCall() {
return is_one_of(Caller, Callee);
}
};
} // namespace clice

View File

@@ -17,12 +17,9 @@ namespace clice {
/// completion, you cannot get go-to-definition, etc. To avoid this, we just use /// completion, you cannot get go-to-definition, etc. To avoid this, we just use
/// some heuristics to simplify the dependent names as normal type/expression. /// some heuristics to simplify the dependent names as normal type/expression.
/// For example, `std::vector<T>::value_type` can be simplified as `T`. /// For example, `std::vector<T>::value_type` can be simplified as `T`.
///
/// Thread safety: NOT thread-safe. Each compilation unit should have its own resolver.
/// The `resolved` cache persists across multiple resolve() calls on the same unit.
class TemplateResolver { class TemplateResolver {
public: public:
explicit TemplateResolver(clang::Sema& sema) : sema(sema) {} TemplateResolver(clang::Sema& sema) : sema(sema) {}
clang::QualType resolve(clang::QualType type); clang::QualType resolve(clang::QualType type);
@@ -30,7 +27,7 @@ public:
void resolve(clang::UnresolvedLookupExpr* expr); void resolve(clang::UnresolvedLookupExpr* expr);
// TODO: Use a clearer approach for resolving UnresolvedLookupExpr. // TODO: use a relative clear way to resolve `UnresolvedLookupExpr`.
void resolve(clang::UnresolvedUsingType* type); void resolve(clang::UnresolvedUsingType* type);
@@ -53,7 +50,7 @@ public:
if(identifier) { if(identifier) {
return lookup(template_name.getQualifier(), identifier); return lookup(template_name.getQualifier(), identifier);
} else { } else {
/// TODO: Operators don't have an IdentifierInfo; need DeclarationName-based lookup. /// FIXME: Operators does't have a name.
return {}; return {};
} }
} }
@@ -63,7 +60,7 @@ public:
} }
lookup_result lookup(const clang::UnresolvedLookupExpr* expr) { lookup_result lookup(const clang::UnresolvedLookupExpr* expr) {
/// TODO: Only returns the first TemplateDecl; should handle overloaded lookups. /// FIXME:
for(auto decl: expr->decls()) { for(auto decl: expr->decls()) {
if(auto TD = llvm::dyn_cast<clang::TemplateDecl>(decl)) { if(auto TD = llvm::dyn_cast<clang::TemplateDecl>(decl)) {
return lookup_result(TD); return lookup_result(TD);
@@ -77,8 +74,8 @@ public:
return {}; return {};
} }
/// TODO: Implement dependent member expression lookup (e.g. `x.template foo<T>()`). /// TODO:
lookup_result lookup(const clang::CXXDependentScopeMemberExpr* expr) { lookup_result lookup(clang::CXXDependentScopeMemberExpr* expr) {
return {}; return {};
} }
@@ -86,19 +83,16 @@ public:
return lookup(decl->getQualifier(), decl->getDeclName()); return lookup(decl->getQualifier(), decl->getDeclName());
} }
lookup_result lookup(const clang::UnresolvedUsingTypenameDecl* decl) { lookup_result resolve(const clang::UnresolvedUsingTypenameDecl* decl) {
return lookup(decl->getQualifier(), decl->getDeclName()); return lookup(decl->getQualifier(), decl->getDeclName());
} }
#ifndef NDEBUG
static inline bool debug = false;
#endif
private: private:
clang::Sema& sema; clang::Sema& sema;
/// Cache of resolved dependent types, keyed by AST node pointer.
/// Shared across resolve() calls within the same TU for performance.
/// This is safe because a given AST node (DependentNameType*, etc.) has a
/// unique identity within the TU — the same pointer always refers to the same
/// syntactic occurrence. Different syntactic occurrences of the "same" type
/// have different AST node pointers.
llvm::DenseMap<const void*, clang::QualType> resolved; llvm::DenseMap<const void*, clang::QualType> resolved;
}; };

View File

@@ -2,7 +2,7 @@
#include <stack> #include <stack>
#include "syntax/token.h" #include "SourceCode.h"
#include "llvm/ADT/SmallVector.h" #include "llvm/ADT/SmallVector.h"
#include "clang/AST/ASTTypeTraits.h" #include "clang/AST/ASTTypeTraits.h"

View File

@@ -1,10 +1,10 @@
#pragma once #pragma once
#include "semantic/ast_utility.h" #include "FilterASTVisitor.h"
#include "semantic/filtered_ast_visitor.h" #include "RelationKind.h"
#include "semantic/relation_kind.h" #include "Resolver.h"
#include "semantic/resolver.h" #include "SymbolKind.h"
#include "semantic/symbol_kind.h" #include "Utility.h"
namespace clice { namespace clice {
@@ -131,6 +131,33 @@ public:
} }
} }
} }
// if(auto module = unit.context().getCurrentNamedModule()) {
// auto keyword = module->DefinitionLoc;
// auto begin = TB.spelledTokenContaining(keyword);
// // assert(begin->kind() == clang::tok::identifier && begin->text(SM) == "module" &&
// // "Invalid module declaration");
//
// begin += 1;
// auto end = TB.spelledTokens(unit.file_id(keyword)).end();
//
// for(auto iter = begin; iter != end; ++iter) {
// if(iter->kind() == clang::tok::identifier) {
// if(auto next = iter + 1; next != end && (next->kind() == clang::tok::period ||
// next->kind() == clang::tok::colon)) {
// iter += 1;
// continue;
// }
//
// end = iter + 1;
// break;
// }
//
// std::unreachable();
// }
//
// handleModuleOccurrence(keyword, llvm::ArrayRef<clang::syntax::Token>(begin, end));
//}
} }
public: public:

193
include/AST/SourceCode.h Normal file
View File

@@ -0,0 +1,193 @@
#pragma once
#include <tuple>
#include "clang/Basic/SourceLocation.h"
#include "clang/Lex/Token.h"
namespace std {
template <>
struct tuple_size<clang::SourceRange> : std::integral_constant<std::size_t, 2> {};
template <>
struct tuple_element<0, clang::SourceRange> {
using type = clang::SourceLocation;
};
template <>
struct tuple_element<1, clang::SourceRange> {
using type = clang::SourceLocation;
};
} // namespace std
namespace clang {
/// Through ADL, make `clang::SourceRange` could be destructured.
template <std::size_t I>
clang::SourceLocation get(clang::SourceRange range) {
if constexpr(I == 0) {
return range.getBegin();
} else {
return range.getEnd();
}
}
class Lexer;
} // namespace clang
namespace clice {
struct LocalSourceRange {
/// The begin position offset to the source file.
uint32_t begin = static_cast<uint32_t>(-1);
/// The end position offset to the source file.
uint32_t end = static_cast<uint32_t>(-1);
constexpr bool operator==(const LocalSourceRange& other) const = default;
constexpr auto length() {
return end - begin;
}
constexpr bool contains(uint32_t offset) const {
return offset >= begin && offset <= end;
}
constexpr bool intersects(const LocalSourceRange& other) const {
return begin <= other.end && end >= other.begin;
}
constexpr bool valid() const {
return begin != -1 && end != -1;
}
};
using TokenKind = clang::tok::TokenKind;
struct Token {
/// Whether this token is at the start of line.
bool is_at_start_of_line = false;
/// Whether this token is a preprocessor directive.
bool is_pp_keyword = false;
/// The kind of this token.
TokenKind kind;
/// The source range of this token.
LocalSourceRange range;
bool valid() {
return range.valid();
}
llvm::StringRef name() const {
return clang::tok::getTokenName(kind);
}
llvm::StringRef text(llvm::StringRef content) const {
assert(range.valid() && "Invalid source range");
return content.substr(range.begin, range.end - range.begin);
}
bool is_eod() const {
return kind == clang::tok::eod;
}
bool is_eof() const {
return kind == clang::tok::eof;
}
bool is_identifier() const {
return kind == clang::tok::raw_identifier;
}
bool is_directive_hash() const {
return is_at_start_of_line && kind == clang::tok::hash;
}
/// The tokens after the include directive are regarded as
/// a whole token, whose kind is `header_name`. For example
/// `<iostream>` and `"test.h"` are both header name.
bool is_header_name() const {
return kind == clang::tok::header_name;
}
};
class Lexer {
public:
Lexer(llvm::StringRef content,
bool ignore_comments = true,
const clang::LangOptions* lang_opts = nullptr,
bool ignore_end_of_directive = true);
Lexer(const Lexer&) = delete;
Lexer(Lexer&&) = delete;
Lexer& operator=(const Lexer&) = delete;
Lexer& operator=(Lexer&&) = delete;
~Lexer();
void lex(Token& token);
/// Get the token before this token without moving the lexer.
Token last();
/// Get the token after this token without moving the lexer.
Token next();
/// Advance the lexer and return the next token.
Token advance();
/// Advance the lexer if the next token kind is the param.
std::optional<Token> advance_if(llvm::function_ref<bool(const Token&)> callback);
std::optional<Token> advance_if(llvm::StringRef spelling) {
return advance_if([&](const Token& token) {
return token.is_identifier() && token.text(content) == spelling;
});
}
std::optional<Token> advance_if(TokenKind kind) {
return advance_if([&](const Token& token) { return token.kind == kind; });
}
/// Advance the lexer until meet the specific kind token.
Token advance_until(TokenKind kind);
private:
/// If this is set to false, the lexer will emit tok::eod at the end
/// of directive.
bool ignore_end_of_directive = true;
/// Whether we are lexing the preprocessor directive.
bool parse_pp_keyword = false;
/// Whether we are lexing the header name.
bool parse_header_name = false;
bool module_declaration_context = true;
/// The cache of last token.
Token last_token;
/// The cache of current token.
Token current_token;
/// The cache of next token.
std::optional<Token> next_token;
/// The lexed content.
llvm::StringRef content;
std::unique_ptr<clang::Lexer> lexer;
};
} // namespace clice

View File

@@ -1,9 +1,8 @@
#pragma once #pragma once
#include <cstdint> #include "Support/Enum.h"
#include "clang/AST/Decl.h" #include "clang/AST/Decl.h"
#include "clang/Basic/TokenKinds.h"
namespace clice { namespace clice {
@@ -15,8 +14,8 @@ namespace clice {
/// consistently across our responses to the client and in the index. Users who prefer to stick to /// consistently across our responses to the client and in the index. Users who prefer to stick to
/// standard LSP kinds can map our `SymbolKind` to the corresponding LSP kinds through /// standard LSP kinds can map our `SymbolKind` to the corresponding LSP kinds through
/// configuration. /// configuration.
struct SymbolKind { struct SymbolKind : refl::Enum<SymbolKind, false, uint8_t> {
enum Kind : std::uint8_t { enum Kind : uint8_t {
Comment = 0, ///< C/C++ comments. Comment = 0, ///< C/C++ comments.
Number, ///< C/C++ number literal. Number, ///< C/C++ number literal.
Character, ///< C/C++ character literal. Character, ///< C/C++ character literal.
@@ -51,121 +50,37 @@ struct SymbolKind {
Invalid, Invalid,
}; };
constexpr SymbolKind() = default; using Enum::Enum;
constexpr SymbolKind(Kind kind) : kind_value(kind) {} constexpr inline static auto InvalidEnum = Kind::Invalid;
constexpr explicit SymbolKind(std::uint8_t kind) : kind_value(static_cast<Kind>(kind)) {}
constexpr operator Kind() const {
return kind_value;
}
constexpr std::uint8_t value_of() const {
return static_cast<std::uint8_t>(kind_value);
}
constexpr std::uint8_t value() const {
return value_of();
}
static SymbolKind from(const clang::Decl* decl); static SymbolKind from(const clang::Decl* decl);
static SymbolKind from(const clang::tok::TokenKind kind); static SymbolKind from(const clang::tok::TokenKind kind);
private:
Kind kind_value = Invalid;
}; };
struct SymbolModifiers { struct SymbolModifiers : refl::Enum<SymbolModifiers, true, uint32_t> {
enum Kind : std::uint32_t { enum Kind {
/// Represents that the symbol is a declaration(e.g. function declaration). /// Represents that the symbol is a declaration(e.g. function declaration).
Declaration = 0, Declaration = 0,
/// Represents that the symbol is a definition(e.g. function definition). /// Represents that the symbol is a definition(e.g. function definition).
Definition = 1, Definition,
/// Represents that the symbol is const modified(e.g. `const` variable). /// Represents that the symbol is const modified(e.g. `const` variable).
Const = 2, Const,
/// Represents that the symbol is overloaded(e.g. overloaded functions and operators). /// Represents that the symbol is overloaded(e.g. overloaded functions and operators).
Overloaded = 3, Overloaded,
/// Represents that the symbol is a part of type(e.g. `*` in `int*`). /// Represents that the symbol is a part of type(e.g. `*` in `int*`).
Typed = 4, Typed,
/// Represents that the symbol is a template(e.g. class template or function template). /// Represents that the symbol is a template(e.g. class template or function template).
Templated = 5, Templated,
/// Represents that the symbol is deprecated.
Deprecated = 6,
/// Represents that the symbol is deduced.
Deduced = 7,
/// Represents that the symbol is readonly.
Readonly = 8,
/// Represents that the symbol is static.
Static = 9,
/// Represents that the symbol is abstract.
Abstract = 10,
/// Represents that the symbol is virtual.
Virtual = 11,
/// Represents that the symbol is a dependent name.
DependentName = 12,
/// Represents that the symbol comes from the default library.
DefaultLibrary = 13,
/// Represents that the symbol is used through a mutable reference.
UsedAsMutableReference = 14,
/// Represents that the symbol is used through a mutable pointer.
UsedAsMutablePointer = 15,
/// Represents that the symbol is a constructor or destructor.
ConstructorOrDestructor = 16,
/// Represents that the symbol is user-defined.
UserDefined = 17,
/// Represents that the symbol is function-scoped.
FunctionScope = 18,
/// Represents that the symbol is class-scoped.
ClassScope = 19,
/// Represents that the symbol is file-scoped.
FileScope = 20,
/// Represents that the symbol is global-scoped.
GlobalScope = 21,
}; };
constexpr static std::uint32_t to_mask(Kind kind) { using Enum::Enum;
return std::uint32_t(1) << static_cast<std::uint32_t>(kind);
}
constexpr SymbolModifiers() = default;
constexpr SymbolModifiers(Kind kind) : value(to_mask(kind)) {}
constexpr explicit SymbolModifiers(std::uint32_t bits) : value(bits) {}
constexpr operator std::uint32_t() const {
return value;
}
constexpr bool contains(Kind kind) const {
return (value & to_mask(kind)) != 0;
}
private:
std::uint32_t value = 0;
}; };
} // namespace clice } // namespace clice

10
include/Async/Async.h Normal file
View File

@@ -0,0 +1,10 @@
#pragma once
#include "Event.h"
#include "FileSystem.h"
#include "Gather.h"
#include "Lock.h"
#include "Network.h"
#include "Sleep.h"
#include "ThreadPool.h"
#include "libuv.h"

96
include/Async/Awaiter.h Normal file
View File

@@ -0,0 +1,96 @@
#pragma once
#include <expected>
#include "Task.h"
#include "libuv.h"
namespace clice::async::awaiter {
template <typename Request>
struct uv_base;
template <typename Request>
requires (is_uv_handle_v<Request>)
struct uv_base<Request> {
/// For libuv handles, `uv_close` must be called to release resources.
/// However, `uv_close` can only be async operation. When close is actually called,
/// the promise object may have already been destroyed, leading to undefined behavior
/// such as use-after-free. To avoid this situation, we allocate memory separately
/// for the handle and destroy it in the callback function.
Request& request;
uv_base() : request(*static_cast<Request*>(std::malloc(sizeof(Request)))) {}
~uv_base() {
uv_close(reinterpret_cast<uv_handle_t*>(&request),
[](uv_handle_t* handle) { std::free(handle); });
}
};
template <typename Request>
requires (is_uv_req_v<Request>)
struct uv_base<Request> {
/// For libuv requests, they don't need to be closed. We can lay them on the promise
/// object directly.
Request request;
};
/// The CRTP base class for the awaiter of libuv async operations. The Derived should
/// implement the `start` and `cleanup` functions.
template <typename Derived, typename Request, typename Ret, typename... Extras>
struct uv : uv_base<Request> {
int error = 0;
promise_base* continuation;
bool await_ready() const noexcept {
return false;
}
/// The callback function to handle the async operation. This should always called
/// in the main thread.
static void callback(Request* request, Extras... extras) {
auto& self = *static_cast<Derived*>(request->data);
/// The derived should implement the cleanup function to release resources or set
/// the error code if the async operation fails.
self.cleanup(extras...);
/// Then we resume the coroutine. It may destroy the current task,
/// If the task is cancelled and disposable.
self.continuation->resume();
}
template <typename Promise>
std::coroutine_handle<> await_suspend(std::coroutine_handle<Promise> waiting) noexcept {
continuation = &waiting.promise();
this->request.data = static_cast<Derived*>(this);
auto& self = *static_cast<Derived*>(this);
/// Start the async operation.
error = self.start(callback);
/// If the async operation fails, resume the coroutine immediately.
if(error < 0) {
return continuation->resume_handle();
}
/// Otherwise, return the coroutine handle to resume later.
return std::noop_coroutine();
}
std::expected<Ret, std::error_code> await_resume() noexcept {
if(error < 0) {
return std::unexpected(std::error_code(error, category()));
}
if constexpr(!std::is_void_v<Ret>) {
return static_cast<Derived*>(this)->result();
} else {
return std::expected<void, std::error_code>();
}
}
};
} // namespace clice::async::awaiter

58
include/Async/Event.h Normal file
View File

@@ -0,0 +1,58 @@
#pragma once
#include "Task.h"
#include "llvm/ADT/ArrayRef.h"
namespace clice::async {
namespace awaiter {
struct event {
bool ready;
llvm::SmallVectorImpl<promise_base*>& awaiters;
bool await_ready() const noexcept {
return ready;
}
template <typename Promise>
void await_suspend(std::coroutine_handle<Promise> handle) const noexcept {
awaiters.emplace_back(&handle.promise());
}
void await_resume() const noexcept {}
};
} // namespace awaiter
class Event {
public:
Event() = default;
void set() {
ready = true;
for(auto* awaiter: awaiters) {
awaiter->schedule();
}
}
void unset() {
ready = false;
}
void clear() {
ready = false;
awaiters.clear();
}
auto operator co_await() {
return awaiter::event{ready, awaiters};
}
private:
bool ready = false;
llvm::SmallVector<promise_base*, 4> awaiters;
};
} // namespace clice::async

View File

@@ -0,0 +1,95 @@
#pragma once
#include <chrono>
#include <cstddef>
#include "Awaiter.h"
#include "Task.h"
#include "libuv.h"
#include "Support/Enum.h"
#include "Support/JSON.h"
#include "llvm/ADT/FunctionExtras.h"
#include "llvm/ADT/StringRef.h"
namespace clice::async {
namespace fs {
struct Mode : refl::Enum<Mode, true> {
enum Kind {
/// Open the file for reading.
Read = 0,
/// Open the file for writing.
Write,
/// Open the file for reading and writing.
ReadWrite,
/// If the file does not exist, create it.
Create,
/// If the file exists, append the data to the end of the file.
Append,
/// If the file exists, truncate the file to zero length.
Truncate,
/// If the file exists, fail the open.
Exclusive,
};
using Enum::Enum;
};
struct handle {
public:
handle(uv_file file) : file(file) {}
handle(const handle&) = delete;
handle(handle&& other) noexcept : file(other.file) {
other.file = -1;
}
~handle();
handle& operator=(const handle&) = delete;
handle& operator=(handle&& other) noexcept = delete;
int value() const {
return file;
}
private:
uv_file file;
};
/// Open the file asynchronously.
Result<handle> open(std::string path, Mode mode);
/// Read the file asynchronously, make sure the buffer is valid until the task is done.
Result<ssize_t> read(const handle& handle, char* buffer, std::size_t size);
Result<std::string> read(std::string path, Mode mode = Mode::Read);
/// Write the file asynchronously, make sure the buffer is valid until the task is done.
Result<void> write(const handle& handle, char* buffer, std::size_t size);
Result<void> write(std::string path,
char* buffer,
std::size_t size,
Mode mode = Mode(Mode::Write, Mode::Create, Mode::Truncate));
struct Stats {
std::chrono::milliseconds mtime;
size_t size;
};
Result<Stats> stat(std::string path);
} // namespace fs
} // namespace clice::async

143
include/Async/Gather.h Normal file
View File

@@ -0,0 +1,143 @@
#pragma once
#include <thread>
#include <tuple>
#include "Event.h"
#include "Task.h"
namespace clice::async {
struct none {};
template <typename Task, typename V = typename std::remove_cvref_t<Task>::value_type>
using task_value_t = std::conditional_t<std::is_void_v<V>, none, V>;
template <typename... Tasks>
auto gather(Tasks&&... tasks) -> Task<std::tuple<task_value_t<Tasks>...>> {
constexpr static std::size_t count = sizeof...(Tasks);
Event event;
std::size_t finished = 0;
auto run_task = [&](auto& task) -> Task<task_value_t<decltype(task)>> {
using V = typename std::remove_cvref_t<decltype(task)>::value_type;
if constexpr(std::is_void_v<V>) {
co_await task;
/// Check if all tasks are finished. If so, set the event to
/// resume the gather handle.
finished += 1;
if(finished == count) {
event.set();
}
co_return none{};
} else {
auto result = co_await task;
finished += 1;
if(finished == count) {
event.set();
}
co_return std::move(result);
}
};
auto schedule_task = [&](auto& task) {
auto core = run_task(task);
core.schedule();
return core;
};
std::tuple all = {schedule_task(tasks)...};
/// Wait for all tasks to finish.
co_await event;
/// Return the results of all tasks.
co_return [&]<std::size_t... Is>(std::index_sequence<Is...>) {
return std::make_tuple(std::get<Is>(all).result()...);
}(std::make_index_sequence<count>{});
}
/// Run the tasks in parallel and return the results.
template <typename... Tasks>
auto run(Tasks&&... tasks) {
auto core = gather(std::forward<Tasks>(tasks)...);
core.schedule();
async::run();
assert(core.done() && "run: not done");
return core.result();
}
template <ranges::input_range Range, typename Coroutine>
requires requires(Coroutine coroutine, ranges::range_value_t<Range> value) {
{ coroutine(value) } -> std::same_as<Task<bool>>;
}
Task<bool> gather(Range&& range,
Coroutine&& coroutine,
std::size_t concurrency = std::thread::hardware_concurrency()) {
std::vector<Task<>> tasks;
tasks.reserve(concurrency);
auto iter = ranges::begin(range);
auto end = ranges::end(range);
Event event;
std::size_t finished = 0;
bool cancelled = false;
auto run_task = [&](auto& value) -> async::Task<> {
/// Execute the first task.
auto task = coroutine(value);
/// If any task fails, cancel all tasks and return false.
if(auto result = co_await task; !result) {
for(auto& task: tasks) {
task.cancel();
task.dispose();
}
cancelled = true;
event.set();
co_return;
}
finished += 1;
/// Check if still have tasks to run. If so, run the next task.
while(iter != end) {
auto task = coroutine(*iter);
iter++;
finished -= 1;
if(auto result = co_await task; !result) {
for(auto& task: tasks) {
task.cancel();
task.dispose();
}
cancelled = true;
event.set();
co_return;
}
finished += 1;
}
/// Check if all tasks are finished. If so, set the event to
/// resume the gather handle.
if(finished == tasks.size()) {
event.set();
}
};
/// Fill tasks.
while(iter != end && tasks.size() < concurrency) {
tasks.emplace_back(run_task(*iter));
tasks.back().schedule();
iter++;
}
co_await event;
co_return !cancelled;
}
} // namespace clice::async

79
include/Async/Lock.h Normal file
View File

@@ -0,0 +1,79 @@
#pragma once
#include "Event.h"
namespace clice::async {
namespace awaiter {
struct lock {
llvm::SmallVectorImpl<promise_base*>& awaiters;
bool await_ready() const noexcept {
return false;
}
template <typename Promise>
void await_suspend(std::coroutine_handle<Promise> handle) const noexcept {
awaiters.emplace_back(&handle.promise());
}
void await_resume() const noexcept {}
};
} // namespace awaiter
class Lock {
friend class guard;
public:
Lock() = default;
class Guard {
public:
Guard(Lock* lock) : lock(lock) {}
Guard(Guard&& other) : lock(other.lock) {
other.lock = nullptr;
}
~Guard() {
if(!lock) {
return;
}
lock->locked = false;
if(!lock->awaiters.empty()) {
lock->awaiters.front()->schedule();
lock->awaiters.erase(lock->awaiters.begin());
}
}
private:
Lock* lock;
};
/// Try to get the lock. If the lock is locked, the current coroutine will be
/// suspended and wait for the lock to be released.
Task<Guard> try_lock() {
/// Note that this task also may be canceled, we make sure
/// even cancel, it can resume one task(through destructor).
Guard guard(this);
if(locked) {
co_await awaiter::lock{awaiters};
}
locked = true;
/// Use `std::move` to make sure it will not resume
/// the awaiter here.
co_return std::move(guard);
}
private:
bool locked = false;
llvm::SmallVector<promise_base*, 4> awaiters;
};
} // namespace clice::async

23
include/Async/Network.h Normal file
View File

@@ -0,0 +1,23 @@
#pragma once
#include "Task.h"
#include "libuv.h"
#include "Support/JSON.h"
#include "llvm/ADT/FunctionExtras.h"
#include "llvm/ADT/StringRef.h"
namespace clice::async::net {
using Callback = llvm::unique_function<Task<void>(json::Value)>;
/// Listen on stdin/stdout, callback is called when there is a LSP message available.
void listen(Callback callback);
/// Listen on the given host and port, callback is called when there is a LSP message available.
void listen(const char* host, unsigned int port, Callback callback);
/// Write a JSON value to the client.
Task<> write(json::Value value);
} // namespace clice::async::net

37
include/Async/Sleep.h Normal file
View File

@@ -0,0 +1,37 @@
#pragma once
#include <chrono>
#include "Awaiter.h"
namespace clice::async {
namespace awaiter {
struct sleep : uv<sleep, uv_timer_t, void> {
std::chrono::milliseconds duration;
int start(auto callback) {
int err = uv_timer_init(async::loop, &request);
if(err < 0) {
return err;
}
return uv_timer_start(&request, callback, duration.count(), 0);
}
void cleanup() {
error = uv_timer_stop(&request);
}
};
} // namespace awaiter
inline auto sleep(std::chrono::milliseconds duration) {
return awaiter::sleep{{}, duration};
}
inline auto sleep(std::size_t milliseconds) {
return sleep(std::chrono::milliseconds(milliseconds));
}
}; // namespace clice::async

334
include/Async/Task.h Normal file
View File

@@ -0,0 +1,334 @@
#pragma once
#include <cassert>
#include <coroutine>
#include <cstdint>
#include <cstdlib>
#include <optional>
#include <source_location>
#include "Support/Format.h"
namespace clice::async {
template <typename T>
class Task;
struct promise_base {
enum Flags : uint8_t {
Empty = 0,
/// The task is cancelled.
Cancelled = 1,
/// The coroutine handle will be destroyed when the task is done or cancelled.
Disposable = 1 << 1,
/// The coroutine is done or is cancelled and resumed, means it will never
/// scheduled again.
Finished = 1 << 2,
};
uint8_t flags;
void* data;
/// The coroutine handle that is waiting for the task to complete.
/// If this is a top-level coroutine, it is empty.
promise_base* continuation = nullptr;
promise_base* next = nullptr;
std::source_location location;
template <typename Promise>
void set(std::coroutine_handle<Promise> handle) {
flags = Empty;
data = handle.address();
}
auto handle() const noexcept {
return std::coroutine_handle<>::from_address(data);
}
void schedule();
bool done() const noexcept {
return handle().done();
}
void destroy() {
handle().destroy();
}
void cancel() {
auto p = this;
while(p) {
p->flags |= Flags::Cancelled;
p = p->next;
}
}
bool cancelled() const noexcept {
return flags & Flags::Cancelled;
}
void dispose() {
flags |= Flags::Disposable;
}
bool disposable() const noexcept {
return flags & Flags::Disposable;
}
void finish() {
flags |= Flags::Finished;
}
bool finished() {
return flags & Flags::Finished;
}
std::coroutine_handle<> resume_handle() {
if(cancelled()) {
/// If the task is cancelled and disposable, destroy the coroutine handle.
auto p = this;
while(p && p->cancelled()) {
auto con = p->continuation;
if(p->disposable()) {
p->destroy();
} else {
p->finish();
}
p = con;
}
return std::noop_coroutine();
} else {
/// Otherwise, resume the coroutine handle.
return handle();
}
}
void resume() {
resume_handle().resume();
}
};
namespace awaiter {
/// The awaiter for the final suspend point of `Task`.
struct final {
promise_base* continuation;
bool await_ready() noexcept {
return false;
}
template <typename Promise>
std::coroutine_handle<> await_suspend(std::coroutine_handle<Promise> current) noexcept {
std::coroutine_handle<> handle = std::noop_coroutine();
/// In the final suspend point, this coroutine is already done.
/// So try to resume the waiting coroutine if it exists.
if(continuation) {
continuation->next = nullptr;
handle = continuation->resume_handle();
}
/// Mark current coroutine as finished.
current.promise().finish();
if(current.promise().disposable()) {
/// If this task is disposable, destroy the coroutine handle.
current.destroy();
}
return handle;
}
void await_resume() noexcept {}
};
/// The awaiter for the `Task` type.
template <typename T, typename P>
struct task {
std::coroutine_handle<P> handle;
bool await_ready() noexcept {
return false;
}
template <typename Promise>
auto await_suspend(std::coroutine_handle<Promise> waiting) noexcept {
/// Store the waiting coroutine in the promise for later scheduling.
/// It will be scheduled in the final suspend point.
assert(!handle.promise().continuation && "await_suspend: already waiting");
handle.promise().continuation = &waiting.promise();
waiting.promise().next = &handle.promise();
/// If this `Task` is awaited from another coroutine, we should schedule
/// the this task first.
return handle.promise().resume_handle();
}
T await_resume() noexcept {
if constexpr(!std::is_void_v<T>) {
assert(handle.promise().value.has_value() && "await_resume: value not set");
return std::move(*handle.promise().value);
}
}
};
} // namespace awaiter
template <typename T = void>
class Task {
public:
template <typename V>
struct promise_result {
std::optional<V> value;
template <typename U>
void return_value(U&& val) noexcept {
assert(!value.has_value() && "return_value: value already set");
value.emplace(std::forward<U>(val));
}
};
// WORKAROUND: GCC bug - full specialization in non-namespace scope not supported
// see: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=85282
template <std::same_as<void> V>
struct promise_result<V> {
void return_void() noexcept {}
};
struct promise_type : promise_base, promise_result<T> {
promise_type(std::source_location location = std::source_location::current()) {
set(handle());
this->location = location;
};
auto get_return_object() {
return Task<T>(handle());
}
auto initial_suspend() {
return std::suspend_always();
}
auto final_suspend() noexcept {
return awaiter::final{continuation};
}
void unhandled_exception() {
std::abort();
}
auto handle() {
return std::coroutine_handle<promise_type>::from_promise(*this);
}
};
using coroutine_handle = std::coroutine_handle<promise_type>;
using value_type = T;
public:
Task() = default;
Task(coroutine_handle handle) : core(handle) {}
Task(const Task&) = delete;
Task(Task&& other) noexcept : core(other.core) {
other.core = nullptr;
}
Task& operator=(const Task&) = delete;
Task& operator=(Task&& other) noexcept {
if(core) {
core.destroy();
}
core = other.core;
other.core = nullptr;
return *this;
}
~Task() {
if(core) {
core.destroy();
}
}
public:
coroutine_handle handle() const noexcept {
return core;
}
coroutine_handle release() noexcept {
auto handle = core;
core = nullptr;
return handle;
}
bool empty() const noexcept {
return !core;
}
bool done() const noexcept {
return core.done();
}
void schedule() {
core.promise().schedule();
}
/// Cancel the task, the suspend point after the current one will be skipped.
void cancel() {
core.promise().cancel();
}
bool cancelled() {
return core.promise().cancelled();
}
/// Dispose the task, it will be destroyed when finished or cancelled.
void dispose() {
core.promise().dispose();
core = nullptr;
}
bool finished() {
return core.promise().finished();
}
T result() {
if constexpr(!std::is_void_v<T>) {
return std::move(core.promise().value.value());
}
}
auto operator co_await() const noexcept {
return awaiter::task<T, promise_type>{core};
}
void stacktrace() {
promise_base* handle = core;
while(handle) {
std::println("{}:{}:{}",
handle->location.file_name(),
handle->location.line(),
handle->location.function_name());
handle = handle->continuation;
}
}
private:
coroutine_handle core;
};
} // namespace clice::async

View File

@@ -0,0 +1,60 @@
#pragma once
#include "Awaiter.h"
namespace clice::async {
namespace awaiter {
template <typename Ret>
struct value {
std::optional<Ret> value;
};
template <>
struct value<void> {};
template <typename Work, typename Ret>
struct thread_pool : value<Ret>, uv<thread_pool<Work, Ret>, uv_work_t, Ret, int> {
Work work;
/// `uv_work_t` has two callback functions, `work_cb` is executed in the thread pool,
/// and `after_work_cb` is executed in the main thread.
static void work_cb(uv_work_t* work) {
auto& awaiter = uv_cast<thread_pool>(work);
if constexpr(!std::is_void_v<Ret>) {
awaiter.value.emplace(awaiter.work());
} else {
awaiter.work();
}
}
int start(auto callback) {
return uv_queue_work(async::loop, &this->request, work_cb, callback);
}
void cleanup(int status) {
this->error = status;
}
Ret result() {
if constexpr(!std::is_void_v<Ret>) {
return std::move(*this->value);
}
}
};
} // namespace awaiter
template <typename Work, typename Ret = std::invoke_result_t<Work>>
async::Task<Ret> submit(Work&& work) {
using W = std::remove_cvref_t<Work>;
auto result = co_await awaiter::thread_pool<W, Ret>{{}, {}, std::forward<Work>(work)};
if(!result) {
/// Thread pool task should never fails.
std::abort();
}
co_return std::move(*result);
}
} // namespace clice::async

79
include/Async/libuv.h Normal file
View File

@@ -0,0 +1,79 @@
#pragma once
#ifdef _WIN32
#define NOMINMAX
#endif
#include "uv.h"
#ifdef _WIN32
#undef THIS
#endif
#include <cassert>
#include <expected>
#include <system_error>
#include <type_traits>
#include "Support/Logging.h"
#include "Support/TypeTraits.h"
namespace clice::async {
/// The default event loop.
extern uv_loop_t* loop;
template <typename T, typename U>
T& uv_cast(U* u) {
assert(u && u->data && "uv_cast: invalid uv handle");
return *static_cast<std::remove_cvref_t<T>*>(u->data);
}
#define UV_TYPE_ITER(_, name) || std::is_same_v<T, uv_##name##_t>
/// Check if the type `T` is a libuv handle.
template <typename T>
constexpr bool is_uv_handle_v = false UV_HANDLE_TYPE_MAP(UV_TYPE_ITER);
/// Check if the type `T` is a libuv request.
template <typename T>
constexpr bool is_uv_req_v = false UV_REQ_TYPE_MAP(UV_TYPE_ITER);
template <typename T>
constexpr bool is_uv_stream_v = std::is_same_v<T, uv_stream_t> || std::is_same_v<T, uv_tcp_t> ||
std::is_same_v<T, uv_pipe_t> || std::is_same_v<T, uv_tty_t>;
#undef UV_TYPE_ITER
template <typename T, typename U>
T* uv_cast(U& u) {
if constexpr(std::is_same_v<T, uv_handle_t>) {
static_assert(is_uv_handle_v<std::remove_cvref_t<U>>, "uv_cast: invalid uv handle");
} else if constexpr(std::is_same_v<T, uv_req_t>) {
static_assert(is_uv_req_v<std::remove_cvref_t<U>>, "uv_cast: invalid uv request");
} else if constexpr(std::is_same_v<T, uv_stream_t>) {
static_assert(is_uv_stream_v<std::remove_cvref_t<U>>, "uv_cast: invalid uv stream");
} else {
static_assert(dependent_false<U>, "uv_cast: invalid type");
}
return reinterpret_cast<T*>(&u);
}
void uv_check_result(const int result,
const std::source_location location = std::source_location::current());
template <typename T>
class Task;
template <typename T>
using Result = Task<std::expected<T, std::error_code>>;
const std::error_category& category();
void init();
void run();
void stop();
} // namespace clice::async

125
include/Compiler/Command.h Normal file
View File

@@ -0,0 +1,125 @@
#pragma once
#include <expected>
#include "Toolchain.h"
#include "Support/Enum.h"
#include "Support/Format.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/DenseSet.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/Support/Allocator.h"
namespace clice {
struct CommandOptions {
/// Ignore unknown commands arguments.
bool ignore_unknown = true;
/// Inject resource directory to the command.
bool resource_dir = false;
/// Query the compiler driver for additional information, such as system includes and target.
bool query_toolchain = false;
/// Suppress the warning log if failed to query driver info.
/// Set true in unittests to avoid cluttering test output.
bool suppress_logging = false;
/// The commands that you want to remove from original commands list.
llvm::ArrayRef<std::string> remove;
/// The commands that you want to add to original commands list.
llvm::ArrayRef<std::string> append;
};
enum class UpdateKind : std::uint8_t {
Unchanged,
Inserted,
Deleted,
};
struct UpdateInfo {
/// The kind of update.
UpdateKind kind;
/// The updated file.
std::uint32_t path_id;
/// The compilation context of this file command, which could
/// be used to identity the same file with different compilation
/// contexts.
const void* context;
};
struct CompilationContext {
/// The working directory of compilation.
llvm::StringRef directory;
/// The compilation arguments.
std::vector<const char*> arguments;
};
std::string print_argv(llvm::ArrayRef<const char*> args);
class CompilationDatabase {
public:
CompilationDatabase();
CompilationDatabase(const CompilationDatabase&) = delete;
CompilationDatabase(CompilationDatabase&& other);
CompilationDatabase& operator=(const CompilationDatabase&) = delete;
CompilationDatabase& operator=(CompilationDatabase&& other);
~CompilationDatabase();
public:
/// Read the compilation database on the give file and return the
/// incremental update infos.
std::vector<UpdateInfo> load_compile_database(llvm::StringRef file);
/// Lookup the compilation context of specific file. If the context
/// param is provided, we will return the compilation context corresponding
/// to the handle. Otherwise we just return the first one(if the file have)
/// multiple compilation contexts.
CompilationContext lookup(llvm::StringRef file,
const CommandOptions& options = {},
const void* context = nullptr);
/// TODO: list all compilation context of the file, this is useful to show
/// all contexts and let user choose one.
/// std::vector<CompilationContext> fetch_all(llvm::StringRef file);
/// Get an the option for specific argument.
static std::optional<std::uint32_t> get_option_id(llvm::StringRef argument);
/// FIXME: bad interface design ...
std::vector<llvm::StringRef> files();
/// FIXME: remove this api?
auto save_string(llvm::StringRef string) -> llvm::StringRef;
#ifdef CLICE_ENABLE_TEST
void add_command(llvm::StringRef directory,
llvm::StringRef file,
llvm::ArrayRef<const char*> arguments);
void add_command(llvm::StringRef directory, llvm::StringRef file, llvm::StringRef command);
/// FIXME: remove this
/// Update commands from json file and return all updated file.
std::expected<std::vector<UpdateInfo>, std::string> load_commands(llvm::StringRef json_content,
llvm::StringRef workspace);
#endif
private:
struct Impl;
std::unique_ptr<Impl> self;
};
} // namespace clice

View File

@@ -1,19 +1,9 @@
#pragma once #pragma once
#include <atomic> #include "CompilationUnit.h"
#include <cassert> #include "Module.h"
#include <cstdint> #include "Preamble.h"
#include <memory> #include "Support/FileSystem.h"
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include "compile/compilation_unit.h"
#include "support/filesystem.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/ADT/StringRef.h"
namespace clang { namespace clang {
@@ -23,49 +13,9 @@ class CodeCompleteConsumer;
namespace clice { namespace clice {
struct PCHInfo {
/// The path of the output PCH file.
std::string path;
/// The building time of this PCH.
std::int64_t mtime;
/// The content used to build this PCH.
std::string preamble;
/// All files involved in building this PCH.
std::vector<std::string> deps;
/// The command arguments used to build this PCH.
std::vector<const char*> arguments;
};
struct ModuleInfo {
/// Whether this module is an interface unit.
/// i.e. has export module declaration.
bool isInterfaceUnit = false;
/// Module name.
std::string name;
/// Dependent modules of this module.
std::vector<std::string> mods;
};
struct PCMInfo : ModuleInfo {
/// PCM file path.
std::string path;
/// Source file path.
std::string srcPath;
/// Files involved in building this PCM(not include module).
std::vector<std::string> deps;
};
struct CompilationParams { struct CompilationParams {
/// The kind of this compilation. /// The kind of this compilation.
CompilationKind kind = CompilationKind::Content; CompilationKind kind;
/// Whether to run clang-tidy. /// Whether to run clang-tidy.
bool clang_tidy = false; bool clang_tidy = false;
@@ -75,13 +25,15 @@ struct CompilationParams {
std::string directory; std::string directory;
bool arguments_from_database = false;
/// Responsible for storing the arguments. /// Responsible for storing the arguments.
std::vector<const char*> arguments; std::vector<const char*> arguments;
llvm::IntrusiveRefCntPtr<vfs::FileSystem> vfs = new ThreadSafeFS(); llvm::IntrusiveRefCntPtr<vfs::FileSystem> vfs = new ThreadSafeFS();
/// Information about reuse PCH. /// Information about reuse PCH.
std::pair<std::string, std::uint32_t> pch; std::pair<std::string, uint32_t> pch;
/// Information about reuse PCM(name, path). /// Information about reuse PCM(name, path).
llvm::StringMap<std::string> pcms; llvm::StringMap<std::string> pcms;

View File

@@ -1,33 +1,17 @@
#pragma once #pragma once
#include <chrono> #include <chrono>
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
#include "compile/diagnostic.h" #include "Directive.h"
#include "compile/directive.h" #include "AST/Resolver.h"
#include "semantic/resolver.h" #include "AST/SourceCode.h"
#include "syntax/token.h" #include "AST/SymbolID.h"
#include "Compiler/Diagnostic.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/DenseSet.h"
#include "clang/Tooling/Syntax/Tokens.h" #include "clang/Tooling/Syntax/Tokens.h"
namespace clice { namespace clice {
namespace index {
// Temporary in-header definition during migration.
struct SymbolID {
std::uint64_t hash;
std::string name;
};
} // namespace index
enum class CompilationKind : std::uint8_t { enum class CompilationKind : std::uint8_t {
/// From preprocessing the source file. Therefore directives /// From preprocessing the source file. Therefore directives
/// are available but AST nodes are not. /// are available but AST nodes are not.

View File

@@ -1,12 +1,9 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <optional>
#include <string> #include <string>
#include "syntax/token.h" #include "AST/SourceCode.h"
#include "llvm/ADT/StringRef.h"
namespace clice { namespace clice {

View File

@@ -1,10 +1,6 @@
#pragma once #pragma once
#include <cstdint> #include "AST/SourceCode.h"
#include <string>
#include <vector>
#include "syntax/token.h"
#include "llvm/ADT/DenseMap.h" #include "llvm/ADT/DenseMap.h"
#include "clang/Lex/MacroInfo.h" #include "clang/Lex/MacroInfo.h"
@@ -20,8 +16,11 @@ struct Include {
/// The file id of included file. /// The file id of included file.
clang::FileID fid; clang::FileID fid;
/// Location of the `include` keyword. /// Location of the `include`.
clang::SourceLocation location; clang::SourceLocation location;
/// The range of filename(includes `""` or `<>`).
clang::SourceRange filename_range;
}; };
/// Information about `__has_include` directive. /// Information about `__has_include` directive.
@@ -37,7 +36,7 @@ struct HasInclude {
/// Information about `#if`, `#ifdef`, `#ifndef`, `#elif`, /// Information about `#if`, `#ifdef`, `#ifndef`, `#elif`,
/// `#elifdef`, `#else`, `#endif` directive. /// `#elifdef`, `#else`, `#endif` directive.
struct Condition { struct Condition {
enum class BranchKind : std::uint8_t { enum class BranchKind : uint8_t {
If = 0, If = 0,
Elif, Elif,
Ifdef, Ifdef,
@@ -50,7 +49,7 @@ struct Condition {
using enum BranchKind; using enum BranchKind;
enum class ConditionValue : std::uint8_t { enum class ConditionValue : uint8_t {
True = 0, True = 0,
False, False,
Skipped, Skipped,
@@ -74,7 +73,7 @@ struct Condition {
/// Information about macro definition, reference and undef. /// Information about macro definition, reference and undef.
struct MacroRef { struct MacroRef {
enum class Kind : std::uint8_t { enum class Kind : uint8_t {
Def = 0, Def = 0,
Ref, Ref,
Undef, Undef,
@@ -94,7 +93,7 @@ struct MacroRef {
/// Information about `#pragma` directive. /// Information about `#pragma` directive.
struct Pragma { struct Pragma {
enum class Kind : std::uint8_t { enum class Kind : uint8_t {
Region, Region,
EndRegion, EndRegion,
@@ -129,39 +128,6 @@ struct Import {
std::vector<clang::SourceLocation> name_locations; std::vector<clang::SourceLocation> name_locations;
}; };
/// Information about `#embed` directive.
struct Embed {
/// The file name in the embed directive, not including quotes or angle brackets.
llvm::StringRef file_name;
/// The actual file that may be embedded by this embed directive.
clang::OptionalFileEntryRef file;
/// Whether the file name is angled.
bool is_angled;
/// Location of the `#` token.
clang::SourceLocation loc;
/// TODO: Currently we do not store parameters of the embed directive.
/// See clang::LexEmbedParametersResult for details.
};
/// Information about `__has_embed` directive.
struct HasEmbed {
/// The file name in the embed directive, not including quotes or angle brackets.
llvm::StringRef file_name;
/// The actual file that may be embedded by this embed directive.
clang::OptionalFileEntryRef file;
/// Whether the file name is angled.
bool is_angled;
/// Location of the `__has_embed` token.
clang::SourceLocation loc;
};
struct Directive { struct Directive {
std::vector<Include> includes; std::vector<Include> includes;
std::vector<HasInclude> has_includes; std::vector<HasInclude> has_includes;
@@ -169,8 +135,6 @@ struct Directive {
std::vector<MacroRef> macros; std::vector<MacroRef> macros;
std::vector<Pragma> pragmas; std::vector<Pragma> pragmas;
std::vector<Import> imports; std::vector<Import> imports;
std::vector<Embed> embeds;
std::vector<HasEmbed> has_embeds;
}; };
} // namespace clice } // namespace clice

46
include/Compiler/Module.h Normal file
View File

@@ -0,0 +1,46 @@
#pragma once
#include <expected>
#include <string>
#include <vector>
#include "Support/Struct.h"
namespace clice {
class CompilationUnit;
struct CompilationParams;
struct ModuleInfo {
/// Whether this module is an interface unit.
/// i.e. has export module declaration.
bool isInterfaceUnit = false;
/// Module name.
std::string name;
/// Dependent modules of this module.
std::vector<std::string> mods;
};
inherited_struct(PCMInfo, ModuleInfo) {
/// PCM file path.
std::string path;
/// Source file path.
std::string srcPath;
/// Files involved in building this PCM(not include module).
std::vector<std::string> deps;
};
/// If input file is module interface unit, return its module name.
/// Otherwise, return an empty string.
std::string scanModuleName(CompilationParams& params);
/// Run the preprocessor to scan the given module unit to
/// collect its module name and dependencies.
std::expected<ModuleInfo, std::string> scanModule(CompilationParams& params);
} // namespace clice

View File

@@ -0,0 +1,40 @@
#pragma once
#include <expected>
#include <string>
#include <vector>
#include "llvm/ADT/StringRef.h"
namespace clice {
class CompilationUnit;
struct CompilationParams;
struct PCHInfo {
/// The path of the output PCH file.
std::string path;
/// The building time of this PCH.
std::int64_t mtime;
/// The content used to build this PCH.
std::string preamble;
/// All files involved in building this PCH.
std::vector<std::string> deps;
/// The command arguments used to build this PCH.
std::vector<const char*> arguments;
};
/// Compute the preamble bound of given content. We just
/// run lex until we find first not directive.
std::uint32_t compute_preamble_bound(llvm::StringRef content);
/// Same as above, but return a group of bounds for chained PCH
/// building.
std::vector<uint32_t> compute_preamble_bounds(llvm::StringRef content);
} // namespace clice

34
include/Compiler/Scan.h Normal file
View File

@@ -0,0 +1,34 @@
#pragma once
#include <string>
#include <vector>
#include "AST/SourceCode.h"
#include "llvm/ADT/StringRef.h"
namespace clice {
struct Inclusion {
/// Whether this file is braced angles.
bool angled;
/// The line of this inclusion(zero based).
/// std::uint32_t line;
/// The included file.
llvm::StringRef file;
};
struct ScanResult {
/// The module file of this file(may be empty).
std::vector<Token> module_name;
/// The includes of file.
std::vector<Inclusion> includes;
};
/// Scan the file and return necessary info.
ScanResult scan(llvm::StringRef content);
} // namespace clice

View File

@@ -1,7 +1,5 @@
#pragma once #pragma once
#include <vector>
#include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/STLExtras.h" #include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/StringRef.h" #include "llvm/ADT/StringRef.h"

View File

@@ -1,3 +1 @@
#pragma once #pragma once
int a = 1;

View File

@@ -0,0 +1,107 @@
#pragma once
#include <cstdint>
#include <vector>
#include "AST/SourceCode.h"
#include "llvm/ADT/StringRef.h"
namespace clice {
struct CompilationParams;
namespace config {
struct CodeCompletionOption {
/// Insert placeholder for keywords? function call parameters? template arguments?
bool enable_keyword_snippet = false;
/// Also apply for lambda ...
bool enable_function_arguments_snippet = false;
bool enable_template_arguments_snippet = false;
bool insert_paren_in_function_call = false;
/// TODO: Add more detailed option, see
/// https://github.com/llvm/llvm-project/issues/63565
bool bundle_overloads = true;
/// The limits of code completion, 0 is non limit.
std::uint32_t limit = 0;
};
}; // namespace config
namespace feature {
enum class CompletionItemKind {
None = 0,
Text,
Method,
Function,
Constructor,
Field,
Variable,
Class,
Interface,
Module,
Property,
Unit,
Value,
Enum,
Keyword,
Snippet,
Color,
File,
Reference,
Folder,
EnumMember,
Constant,
Struct,
Event,
Operator,
TypeParameter
};
/// Represents a single code completion item to be presented to the user.
struct CompletionItem {
/// The primary label displayed in the completion list.
std::string label;
/// Additional details, like a function signature, shown next to the label.
std::string detail;
/// A short description of the item, typically its type or namespace.
std::string description;
/// Full documentation for the item, shown on selection or hover.
std::string document;
/// The kind of item (function, class, etc.), used for an icon.
CompletionItemKind kind;
/// A score for ranking this item against others. Higher is better.
float score;
/// Whether this item is deprecated (often rendered with a strikethrough).
bool deprecated;
/// The text edit to be applied when this item is accepted.
struct Edit {
/// The new text to insert, which may be a snippet.
std::string text;
/// The source range to be replaced by the new text.
LocalSourceRange range;
} edit;
};
using CodeCompletionResult = std::vector<CompletionItem>;
std::vector<CompletionItem> code_complete(CompilationParams& params,
const config::CodeCompletionOption& option);
} // namespace feature
} // namespace clice

View File

@@ -1,3 +1 @@
#pragma once #pragma once
int b = 2;

View File

@@ -0,0 +1,19 @@
#pragma once
#include "Compiler/Diagnostic.h"
#include "Server/Convert.h"
#include "Support/JSON.h"
namespace clice {
class CompilationUnitRef;
}
namespace clice::feature {
/// FIXME: This is not correct way, we don't want to couple
/// `Feature with Protocol`? Return an array of LSP diagnostic.
json::Value diagnostics(PositionEncodingKind kind, PathMapping mapping, CompilationUnitRef unit);
} // namespace clice::feature

View File

@@ -0,0 +1,3 @@
#pragma once
namespace clice::proto {}

View File

@@ -0,0 +1,26 @@
#pragma once
#include <vector>
#include "AST/SourceCode.h"
#include "Index/Shared.h"
namespace clice::feature {
struct DocumentLink {
/// The range of the whole link.
LocalSourceRange range;
/// The target string path.
std::string file;
};
using DocumentLinks = std::vector<DocumentLink>;
/// Generate document link for main file.
DocumentLinks document_links(CompilationUnitRef unit);
/// Generate document link for all source file.
index::Shared<DocumentLinks> index_document_link(CompilationUnitRef unit);
} // namespace clice::feature

View File

@@ -0,0 +1,37 @@
#pragma once
#include "AST/SourceCode.h"
#include "AST/SymbolKind.h"
#include "Index/Shared.h"
namespace clice::feature {
struct DocumentSymbol {
/// The range of symbol name in source code.
LocalSourceRange selectionRange;
/// The range of whole symbol.
LocalSourceRange range;
/// The symbol kind of this document symbol.
SymbolKind kind;
/// The symbol name.
std::string name;
/// Extra information about this symbol.
std::string detail;
/// The symbols that this symbol contains
std::vector<DocumentSymbol> children;
};
using DocumentSymbols = std::vector<DocumentSymbol>;
/// Generate document symbols for only interested file.
DocumentSymbols document_symbols(CompilationUnitRef unit);
/// Generate document symbols for all file in unit.
index::Shared<DocumentSymbols> index_document_symbol(CompilationUnitRef unit);
} // namespace clice::feature

View File

@@ -0,0 +1,55 @@
#pragma once
#include "AST/SourceCode.h"
#include "Index/Shared.h"
#include "Support/Enum.h"
namespace clice::feature {
struct FoldingRangeKind : refl::Enum<FoldingRangeKind> {
enum Kind : uint8_t {
Invalid = 0,
Comment,
Imports,
Region,
Namespace,
Class,
Enum,
Struct,
Union,
LambdaCapture,
FunctionParams,
FunctionBody,
FunctionCall,
CompoundStmt,
AccessSpecifier,
ConditionDirective,
Initializer,
};
using Enum::Enum;
constexpr static auto InvalidEnum = Invalid;
};
/// We don't record the coalesced text for a range, because it's rarely useful.
struct FoldingRange {
/// The range to fold.
LocalSourceRange range;
/// Describes the kind of the folding range.
FoldingRangeKind kind;
/// The text to display when the folding range is collapsed.
std::string text;
};
using FoldingRanges = std::vector<FoldingRange>;
/// Generate folding range for interested file only.
FoldingRanges folding_ranges(CompilationUnitRef unit);
/// Generate folding range for all files.
index::Shared<FoldingRanges> index_folding_range(CompilationUnitRef unit);
} // namespace clice::feature

View File

@@ -0,0 +1,14 @@
#pragma once
#include "AST/SourceCode.h"
#include "Protocol/Feature/Formatting.h"
#include "llvm/ADT/StringRef.h"
namespace clice::feature {
std::vector<proto::TextEdit> document_format(llvm::StringRef file,
llvm::StringRef content,
std::optional<LocalSourceRange>);
}

74
include/Feature/Hover.h Normal file
View File

@@ -0,0 +1,74 @@
#pragma once
#include "AST/SourceCode.h"
#include "AST/SymbolKind.h"
#include "Index/Shared.h"
namespace clice::config {
struct HoverOptions {
/// Strip doxygen info and merge with lsp info
bool enable_doxygen_parsing = true;
/// If set `false`, the comment will be wrapped
/// in code block and keep ascii typesetting
bool parse_comment_as_markdown = true;
/// Show sugar type
bool show_aka = true;
};
} // namespace clice::config
namespace clice::feature {
struct HoverItem {
enum class HoverKind : uint8_t {
/// The typename of a variable or a type alias.
Type,
/// Size of type or variable.
Size,
/// Align of type or variable.
Align,
/// Offset of field in a class/struct.
Offset,
/// Bit width of a bit field.
BitWidth,
/// The index of a field in a class/struct.
FieldIndex,
/// The value of an enum item.
EnumValue,
};
using enum HoverKind;
HoverKind kind;
std::string value;
};
/// Hover information for a symbol.
struct Hover {
/// Title
SymbolKind kind;
std::string name;
/// Extra information.
std::vector<HoverItem> items;
/// Raw document in the source code.
std::string document;
/// The full qualified name of the declaration.
std::string qualifier;
/// The source code of the declaration.
std::string source;
};
/// Generate the hover information for the given declaration(for test).
Hover hover(CompilationUnitRef unit, const clang::NamedDecl* decl);
/// Generate the hover information for the symbol at the given offset.
Hover hover(CompilationUnitRef unit, std::uint32_t offset);
} // namespace clice::feature

View File

@@ -0,0 +1,57 @@
#pragma once
#include "AST/SourceCode.h"
#include "AST/SymbolID.h"
#include "Index/Shared.h"
#include "Support/JSON.h"
namespace clice::config {
struct InlayHintsOptions {
/// If false, inlay hints are completely disabled.
bool enabled = true;
// Whether specific categories of hints are enabled.
bool parameters = true;
bool deduced_types = true;
bool designators = true;
bool block_end = false;
bool default_arguments = false;
// Limit the length of type names in inlay hints. (0 means no limit)
uint32_t type_name_limit = 32;
};
} // namespace clice::config
namespace clice::feature {
enum class InlayHintKind {
Parameter,
InvalidEnum,
DefaultArgument,
Type,
Designator,
BlockEnd,
};
struct InlayHint {
/// The position offset of the inlay hint in the source code.
std::uint32_t offset;
/// The kind/category of the inlay hint.
InlayHintKind kind;
/// The label parts of the inlay hint.
/// Each SymbolID consists of two parts: the symbol name and its USR hash.
/// For symbols without a USR (e.g., built-in types or function parameters),
/// the symbol hash will be empty.
/// Otherwise, the symbol hash is non-empty and can be used for "go-to-definition".
std::vector<index::SymbolID> parts;
};
auto inlay_hints(CompilationUnitRef unit,
LocalSourceRange target,
const config::InlayHintsOptions& options) -> std::vector<InlayHint>;
} // namespace clice::feature

85
include/Feature/Lookup.h Normal file
View File

@@ -0,0 +1,85 @@
#pragma once
#include "AST/SymbolKind.h"
#include "Server/Protocol.h"
#include "Support/Struct.h"
namespace clice::proto {
struct WorkDoneProgressOptions {
/// Report on work done progress.
bool workDoneProgress = false;
};
struct PartialResultParams {};
/// The options of the all lookup.
using LookupOptions = WorkDoneProgressOptions;
/// The parameters of the simple lookup(definition, declaration,
/// type definition, implementation and reference).
inherited_struct(ReferenceParams, TextDocumentPositionParams, PartialResultParams){};
/// The result of the simple lookup.
using ReferenceResult = std::vector<Location>;
/// The parameters of the all hierarchy resolve(call hierarchy and type hierarchy)
using HierarchyPrepareParams = TextDocumentPositionParams;
struct HierarchyItem {
/// The name of the item.
string name;
/// The kind of the item.
SymbolKind kind;
/// The resource identifier of this item.
DocumentUri uri;
/// The range enclosing this symbol not including leading/trailing whitespace
/// but everything else, e.g. comments and code.
Range range;
/// The range that should be selected and revealed when this symbol is being
/// picked, e.g. the name of a function. Must be contained by the
/// [`range`](#CallHierarchyItem.range).
Range selectionRange;
/// A customized data of the item. We use it to store
/// the USR hash of the item.
uint64_t data = 0;
};
using HierarchyPrepareResult = std::vector<HierarchyItem>;
/// The parameters of the both call hierarchy and type hierarchy.
inherited_struct(HierarchyParams, TextDocumentPositionParams, PartialResultParams) {
HierarchyItem item;
};
struct CallHierarchyIncomingCall {
/// The item that makes the call.
HierarchyItem from;
/// The ranges at which the calls appear. This is relative to the caller
/// denoted by [`this.from`](#CallHierarchyIncomingCall.from).
std::vector<Range> fromRanges;
};
using CallHierarchyIncomingCallsResult = std::vector<CallHierarchyIncomingCall>;
struct CallHierarchyOutgoingCall {
/// The item that is called.
HierarchyItem to;
/// The range at which this item is called. This is the range relative to
/// the caller, e.g the item passed to `callHierarchy/outgoingCalls` request.
std::vector<Range> fromRanges;
};
using CallHierarchyOutgoingCallsResult = std::vector<CallHierarchyOutgoingCall>;
/// The result of the both super and sub type hierarchy.
using TypeHierarchyResult = std::vector<HierarchyItem>;
} // namespace clice::proto

View File

@@ -0,0 +1,29 @@
#pragma once
#include "AST/SourceCode.h"
#include "AST/SymbolKind.h"
#include "Index/Shared.h"
namespace clice::config {
struct SemanticTokensOption {};
}; // namespace clice::config
namespace clice::feature {
struct SemanticToken {
LocalSourceRange range;
SymbolKind kind;
SymbolModifiers modifiers;
};
using SemanticTokens = std::vector<SemanticToken>;
/// Generate semantic tokens for the interested file only.
SemanticTokens semantic_tokens(CompilationUnitRef unit);
/// Generate semantic tokens for all files.
index::Shared<SemanticTokens> index_semantic_token(CompilationUnitRef unit);
} // namespace clice::feature

View File

@@ -0,0 +1,27 @@
#pragma once
#include <cstdint>
#include <vector>
#include "Protocol/Feature/SignatureHelp.h"
#include "llvm/ADT/StringRef.h"
namespace clice {
struct CompilationParams;
namespace config {
struct SignatureHelpOption {};
} // namespace config
namespace feature {
proto::SignatureHelp signature_help(CompilationParams& params,
const config::SignatureHelpOption& option);
} // namespace feature
} // namespace clice

View File

@@ -1,11 +1,8 @@
#pragma once #pragma once
#include <cassert>
#include <cstdint>
#include <string>
#include <vector> #include <vector>
#include "syntax/token.h" #include "AST/SourceCode.h"
#include "llvm/ADT/DenseMap.h" #include "llvm/ADT/DenseMap.h"
@@ -57,7 +54,7 @@ struct IncludeGraph {
return it->second; return it->second;
} }
std::uint32_t path_id(clang::FileID fid) const { std::uint32_t path_id(clang::FileID fid) {
auto include = include_location_id(fid); auto include = include_location_id(fid);
if(include != -1) { if(include != -1) {
return locations[include].path_id; return locations[include].path_id;

View File

@@ -1,11 +1,6 @@
#pragma once #pragma once
#include <chrono> #include "TUIndex.h"
#include <cstdint>
#include <memory>
#include <vector>
#include "index/tu_index.h"
#include "llvm/Support/Allocator.h" #include "llvm/Support/Allocator.h"
#include "llvm/Support/MemoryBuffer.h" #include "llvm/Support/MemoryBuffer.h"
@@ -64,23 +59,15 @@ public:
/// Remove the index of specific path id. /// Remove the index of specific path id.
void remove(this Self& self, std::uint32_t path_id); void remove(this Self& self, std::uint32_t path_id);
/// Get the stored source content for position mapping.
llvm::StringRef content(this const Self& self);
/// Merge the index with given compilation context. /// Merge the index with given compilation context.
void merge(this Self& self, void merge(this Self& self,
std::uint32_t path_id, std::uint32_t path_id,
std::chrono::milliseconds build_at, std::chrono::milliseconds build_at,
std::vector<IncludeLocation> include_locations, std::vector<IncludeLocation> include_locations,
FileIndex& index, FileIndex& index);
llvm::StringRef content);
/// Merge the index with given header context. /// Merge the index with given header context.
void merge(this Self& self, void merge(this Self& self, std::uint32_t path_id, std::uint32_t include_id, FileIndex& index);
std::uint32_t path_id,
std::uint32_t include_id,
FileIndex& index,
llvm::StringRef content);
friend bool operator==(MergedIndex& lhs, MergedIndex& rhs); friend bool operator==(MergedIndex& lhs, MergedIndex& rhs);

View File

@@ -1,16 +1,6 @@
#pragma once #pragma once
#include <algorithm> #include "TUIndex.h"
#include <cstdint>
#include <vector>
#include "index/tu_index.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/Allocator.h"
namespace clice::index { namespace clice::index {
@@ -30,17 +20,6 @@ struct PathPool {
auto path_id(llvm::StringRef path) { auto path_id(llvm::StringRef path) {
assert(!path.empty()); assert(!path.empty());
// Normalize backslashes to forward slashes so that paths from different
// sources (URI decoding, CDB, clang FileManager) compare equal on
// Windows where native separators are backslashes.
llvm::SmallString<256> normalized;
if(path.contains('\\')) {
normalized = path;
std::replace(normalized.begin(), normalized.end(), '\\', '/');
path = normalized;
}
auto [it, success] = cache.try_emplace(path, paths.size()); auto [it, success] = cache.try_emplace(path, paths.size());
if(!success) { if(!success) {
return it->second; return it->second;
@@ -55,18 +34,6 @@ struct PathPool {
llvm::StringRef path(std::uint32_t id) { llvm::StringRef path(std::uint32_t id) {
return paths[id]; return paths[id];
} }
/// Look up a path in the cache, normalizing backslashes first.
/// Returns cache.end() if the path is not interned.
auto find(llvm::StringRef path) {
llvm::SmallString<256> normalized;
if(path.contains('\\')) {
normalized = path;
std::replace(normalized.begin(), normalized.end(), '\\', '/');
path = normalized;
}
return cache.find(path);
}
}; };
struct FileInfo { struct FileInfo {

View File

@@ -1,18 +1,12 @@
#pragma once #pragma once
#include <array>
#include <bit>
#include <chrono> #include <chrono>
#include <cstdint>
#include <string>
#include <vector>
#include "index/include_graph.h" #include "IncludeGraph.h"
#include "semantic/relation_kind.h" #include "AST/RelationKind.h"
#include "semantic/symbol_kind.h" #include "AST/SourceCode.h"
#include "support/bitmap.h" #include "AST/SymbolKind.h"
#include "Support/Bitmap.h"
#include "llvm/Support/raw_ostream.h"
namespace clice::index { namespace clice::index {
@@ -79,17 +73,9 @@ struct TUIndex {
llvm::DenseMap<clang::FileID, FileIndex> file_indices; llvm::DenseMap<clang::FileID, FileIndex> file_indices;
/// File indices keyed by path_id, populated by from() for deserialized data.
/// When built from AST, this is empty and file_indices (keyed by FileID) is used.
llvm::DenseMap<std::uint32_t, FileIndex> path_file_indices;
FileIndex main_file_index; FileIndex main_file_index;
static TUIndex build(CompilationUnitRef unit, bool interested_only = false); static TUIndex build(CompilationUnitRef unit);
void serialize(llvm::raw_ostream& os) const;
static TUIndex from(const void* data);
}; };
} // namespace clice::index } // namespace clice::index

Some files were not shown because too many files have changed in this diff Show More