Skip to content

Compilation

增量解析

每当你修改代码时,clice 都必须重新解析文件。clice 使用一种叫做 preamble 的机制实现增量编译。Preamble 是 Precompiled Header 的一种特殊情况——它将文件开头的预处理指令(#include 块)构建成 PCH 缓存在磁盘上。后续解析时直接加载 PCH,跳过 preamble 部分。

例如:

cpp
#include <iostream>

int main () {
    std::cout << "Hello world!" << std::endl;
}

iostream 头文件展开后约有 2 万行代码。clice 会先把 #include <iostream> 构建成 PCH 文件,后续重新解析的代码量就只剩 5 行。除非你修改了 preamble 部分的代码(增删 include),才需要重建 PCH。

新鲜度检测

PCH 的新鲜度通过两层机制检查:

  1. mtime 检查 — 快速路径:如果没有依赖文件的 mtime 比缓存的 PCH 更新,则仍然有效。
  2. 内容哈希 — 如果 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 的宽限期。如果宽限期后仍为零,则触发编译的取消令牌。

宽限期防止抖动——快速的打开/关闭序列不会导致冗余的取消-重启循环。

取代逻辑

当文件在其先前的编译仍在进行时被编辑,旧的构建会被"取代"——其兴趣被释放,新的构建开始。旧的工作进程任务观察到取消令牌后提前终止。