Compilation
增量解析
每当你修改代码时,clice 都必须重新解析文件。clice 使用一种叫做 preamble 的机制实现增量编译。Preamble 是 Precompiled Header 的一种特殊情况——它将文件开头的预处理指令(#include 块)构建成 PCH 缓存在磁盘上。后续解析时直接加载 PCH,跳过 preamble 部分。
例如:
#include <iostream>
int main () {
std::cout << "Hello world!" << std::endl;
}iostream 头文件展开后约有 2 万行代码。clice 会先把 #include <iostream> 构建成 PCH 文件,后续重新解析的代码量就只剩 5 行。除非你修改了 preamble 部分的代码(增删 include),才需要重建 PCH。
新鲜度检测
PCH 的新鲜度通过两层机制检查:
- mtime 检查 — 快速路径:如果没有依赖文件的 mtime 比缓存的 PCH 更新,则仍然有效。
- 内容哈希 — 如果 mtime 发生了变化(例如
touch但没有实际修改),则哈希文件内容。只有哈希不同时才重建 PCH。
这避免了仅时间戳变化(构建系统常见的原地重新生成文件)时的昂贵重建。
多进程流水线
编译任务分布在多个工作进程中:
- 无状态工作进程处理 PCH 构建、PCM 构建(C++20 模块)、补全和签名帮助。这些是临时性的——请求之间不保持状态。
- 有状态工作进程在内存中持有已解析的 AST,服务查询(hover、semantic tokens、inlay hints 等)。文件通过 LRU 淘汰机制缓存。
主进程不直接执行编译。它分发任务并通过 stdin/stdout 上的 bincode IPC 收集结果。
C++20 模块
模块编译遵循依赖图(CompileGraph)。主进程从语法级的 DependencyGraph 解析模块依赖,然后通过无状态工作进程按拓扑排序构建 PCM 文件。
每个模块单元在编译图中有一个 CompileUnit,包含:
- 引用计数(兴趣计数),追踪有多少下游依赖需要它
- 取消令牌,当引用计数归零时触发
当文件被修改时,依赖的模块单元会按依赖顺序自动重新编译。
取消编译
clice 使用基于兴趣计数的取消来避免在不再需要的编译上浪费 CPU。
问题
在模块 DAG 如 A → B → C 中,如果用户打开文件 C,编译会依次启动 A、B、C。但如果用户在 A 还在构建时关闭了 C(或切换到另一个文件),继续构建 B 和 C 是没有意义的。
机制
编译图中的每个 CompileUnit 维护一个引用计数:
- 当文件需要某个模块的 PCM 时,增加(
acquire)该模块的引用计数。 - 当不再需要时(文件关闭、编辑被取代),减少(
release)引用计数。 - 当引用计数归零时,触发一个单 tick 的宽限期。如果宽限期后仍为零,则触发编译的取消令牌。
宽限期防止抖动——快速的打开/关闭序列不会导致冗余的取消-重启循环。
取代逻辑
当文件在其先前的编译仍在进行时被编辑,旧的构建会被"取代"——其兴趣被释放,新的构建开始。旧的工作进程任务观察到取消令牌后提前终止。