Skip to content

RFC-029a: 模块缓存与增量重编译 ​

摘要 ​

兑现 RFC-029「子 RFC 规划」预留的 029a 行:在已稳定的编排器之上,为编译器定义 分层缓存与增量重编译语义——L1 进程内分析结果、L2 会话内模块级、L3 跨进程磁盘三层, 配统一的键定义、失效规则与强制度量钩子,收敛当前四处互不相通的缓存散点(#251 P0-6 审计)。

动机 ​

宿主与边界依据 ​

RFC-029(已接受)明确将本主题划出自身边界,并预留了本子 RFC 的槽位:

不包含:缓存、文件监听、热重载、增量重编译、跨包循环依赖处理。 —— RFC-029 §核心原则

子 RFC能力前提
029a模块缓存与增量重编译编排器稳定

前提「编排器稳定」已满足:编排器随 RFC-029 落地并经 #232/#243/#244/#245 交付验证。

当前的问题 ​

缓存散点互不相通:

已有层实际消费方问题
VALIDATE_CACHE(validate.rs)进程内仅 formatterCLI check/run 与编排器都不走它
Z3Backend 查询缓存检查会话证明管道无失效规则与预算设计(RFC-009a 一句话)
DocumentCache(RFC-017)LSP 会话LSP与 CLI 编译面完全不通
~/.yaoxiang/cache(RFC-014)磁盘包下载与编译无关
代码缓存(RFC-028)运行时JIT非编译期层

多入口重复全链路(2026-09-07 实证):

  • check_files_with_diagnostics 循环内每文件新建 Compiler:同进程校验 19 个库层文件, std 嵌入链被重复 typecheck 19 遍;
  • runtime-error 类测试文件走 check 与 run 两个子进程,全链路做两遍;
  • 借用检查 fast_path_check 每次写操作独立做全图 BFS,无结果缓存(#251 P0-6 审计原始发现; RFC-009a 仅一句「一次 BFS 结果可缓存供同一令牌的多次查询复用」,无机制设计)。

成本解剖(--version 基线法):进程 spawn + CLI 初始化 ~50 ms;裸文件 check ~60 ms (前端编译 ≈ 10 ms);std.test 链文件 ~80 ms(嵌入链 ≈ +20 ms)。测试回路 170 文件实测 9.8 s ≈ 58 ms/文件,~85% 是进程生命周期成本。这决定了本 RFC 的收益定位:消灭多入口重复 与 std 链重复编译;进程 spawn 成本不在缓存射程内(子进程隔离是 RFC-036 §6 的设计选择, 见非目标)。

提案 ​

1. 三层缓存模型 ​

缓存按生命周期分层,每层有唯一宿主与明确条目:

层生命周期宿主典型条目
L1单次检查会话validate_source / 证明管道验证结果(诊断 + AST)、借用 unsafe 集、SMT 查询
L2单次编排(进程内可复用)CompileSessionRegistry、逐模块验证结果
L3跨 run磁盘 cache/compile/模块字节码

分层判据:条目被谁消费、随谁失效。L1 条目只被一次检查消费;L2 条目被一次编排内的多个 入口消费;L3 条目跨进程消费。跨层不共享条目——同一语义产物允许同时存在于多层, 各层独立命中、独立计数。

2. 缓存键:内容哈希即身份 ​

rust
struct CacheKey {
    content_hash: u64,          // FNV-1a,validate.rs 既有实现
    compiler_version: Option<Version>,   // 仅 L3 携带
    config_fingerprint: Option<u64>,     // 仅 L3 携带:影响语义的配置位哈希
}
  • L1/L2 键 = content_hash:源码即身份,同内容必命中,与路径无关;
  • L3 键 = 三元组整体:版本或指纹任一不匹配即 miss,整键重编;
  • 键不齐拒绝写入:L3 条目缺版本或缺指纹一律不落盘——宁可 miss 不落旧产物, 与诊断 i18n 缺翻译拒绝编译同一构造期拒绝原则。

3. L1:validate_source 是唯一前端入口 ​

所有需要「源码 → (诊断, AST)」的组件必须经 validate_source,不允许自建前端链路。 现状它已存在且带进程级缓存,但只有 formatter 消费;本 RFC 将三个绕行者收编:

消费方现状收编后
check_files_with_diagnostics每文件 Compiler::new()经 validate_source 查/回填
编排器逐文件 typecheck自带 Registry 重复构建经 validate_source 查/回填
LSP 诊断独立 DocumentCache 体系经 validate_source(第三波)

借用分析结果缓存是 L1 的第二条线。定义函数检查会话为借用缓存的生命周期单位:

rust
struct FunctionCheckSession {
    /// 全图 BFS 求得的 unsafe 集合——进入函数时建,退出函数时弃
    unsafe_set: HashSet<TokenId>,
    /// SMT 查询缓存(RFC-027 预算内的线性算术查询)
    smt_cache: HashMap<u64, SMTResult>,
}

同一函数的多次写操作共享一次 BFS(落实 RFC-009a 的设计句);SMT 查询缓存收编进同一 会话对象,随会话销毁。会话不跨函数存活——函数内代码不变性由类型检查流程本身保证。

4. L2:会话内模块缓存 ​

编排会话是一等实体,不再每次隐式重建:

rust
pub struct CompileSession {
    /// 键 = (模块路径, content_hash);路径用于区分同名内容的不同文件
    modules: HashMap<(PathBuf, u64), Arc<ModuleEntry>>,
}

struct ModuleEntry {
    validated: Arc<ValidateResult>,
}
  • std 嵌入源(yx_sources include_str!)内容恒定,其哈希进程内计算一次 (LazyLock 静态表)——N 个导入文件共享一次编译;
  • 同进程第二次 compile_project(如测试回路的 check + run、yx_runner 多文件) 逐模块查 CompileSession,内容未变即整模块复用;
  • 会话随进程消亡,无落盘、无跨进程义务。

5. L3:磁盘字节码缓存 ​

text
~/.yaoxiang/cache/compile/<content_hash>-<version>-<fp>.yxbc
  • 产物 = 既有 BytecodeFile 魔数格式(run 的字节码探针路径原样消费), 不发明新序列化格式;
  • 加载 = 读文件 + 校验文件名三元组与内容一致;任一不符按 miss 处理并删除坏条目;
  • 首批只缓存 std 模块:嵌入源内容稳定,版本锚定,命中率恒定;项目模块缓存在 开放问题解决后再启。

6. 失效规则 ​

规则触发动作
R1源内容变化无主动失效——键含内容哈希,旧条目自然不可达
R2依赖模块变化(L2)沿 use 图向下游标记脏,仅重编脏模块及其下游
R3编译器版本变化(L3)全量 miss(键含版本,不做跨版本迁移)
R4键不齐(L3)拒绝写入

失效传播只存在于 L2 依赖图内;L3 不追踪模块间依赖——版本守卫兜底,语义永不依赖 旧产物的"恰好还有效"。

7. 度量:不可观测的缓存不许合入 ​

rust
pub struct CacheStats {
    pub hits: u64,
    pub misses: u64,
    pub cached_bytes: u64,
}

check --json 与 test --json 的 summary 附 cache 字段:

json
{
  "cache": {
    "l1": { "hits": 18, "misses": 1 },
    "l2": { "hits": 0, "misses": 19 },
    "l3": { "hits": 57, "misses": 3, "cached_bytes": 245760 }
  }
}

任何缓存条目合入主干的 PR 必须附带该字段的命中证据,防止「缓存了但没人命中」的 静默腐烂(#289 可观测性对齐)。

详细设计 ​

类型系统影响 ​

无。本 RFC 不引入语言类型、语法或语义变化;缓存条目类型全部复用既有 ValidateResult / ModuleRegistry / BytecodeFile。

运行时行为 ​

  • run 的执行语义不变;L3 命中等价于「从磁盘加载字节码」,走既有 BytecodeFile::probe/load 路径,字节级同构;
  • 缓存全 miss 时,行为与今日完全一致(缓存必须透明——这是验收项,不是愿景);
  • 可观测变化仅两处:耗时下降;check/test 的 --json 输出新增 cache 字段 (纯附加,旧消费者不受影响)。

编译器改动 ​

组件改动
src/frontend/validate.rs成为唯一前端入口(消费方收编,见提案 §3)
src/util/diagnostic/mod.rscheck_files_with_diagnostics 去每文件 Compiler::new()
src/frontend/module/orchestrator.rscompile_project 接 CompileSession(L2)
src/frontend/core/typecheck/proof/FunctionCheckSession(L1 借用/SMT 缓存)
src/frontend/pipeline/compilation_cache.rs新增——L3 读写与 CacheStats(占位测试已预留)
src/frontend/pipeline/incremental_scheduler.rs新增——沿 use 图脏文件判定(占位测试已预留)
src/util/cache.rsDocumentCache 迁为 L1 消费方(第三波,RFC-017 收编)
src/main.rs / src/util/test_runner.rsJSON 报告附 cache 字段

向后兼容性 ​

  • ✅ 语言面零变化,CLI 面零破坏,JSON 字段纯新增;
  • ✅ 缓存层可整体降级(清空即回到无缓存行为),无迁移路径负担;
  • L3 磁盘条目带版本守卫,版本升级后旧条目自然 miss,无需清理工具 (yaoxiang cache clean 语义由 RFC-014 既有命令延伸到 compile/ 段)。

权衡 ​

优点 ​

  • 直接消除三处实测浪费:同进程多入口重复全链路、std 嵌入链 N 次重复编译 (19 文件 × 20 ms → 1 × 20 ms)、借用检查大函数 O(V²) 重复 BFS;
  • 缓存策略单源化:键定义、失效规则、度量口径全仓唯一,五个散点收编进同一套语义;
  • 度量强制使缓存收益与腐烂都可见。

缺点 ​

  • L1 进程级缓存引入共享状态:--parallel 下为锁热点。缓解:条目 Arc 不可变、 键为 u64,锁粒度=单桶;
  • L3 引入「旧产物被加载」的风险面。缓解:版本 + 指纹双守卫,宁 miss 勿错, 且字节码自带魔数校验;
  • L2 使编排器从无状态变有状态,回归面覆盖 #232/#243-245 全量场景。

替代方案 ​

方案为什么不选
只做 L3 磁盘缓存实测 85% 成本在进程生命周期,L3 只触及 std 链 ~20 ms/文件;不解决多入口重复
编译常驻服务(daemon)收益上限最高(连 spawn 一起吃掉),但引入服务生命周期管理,与 RFC-036 子进程隔离模型冲突;留作未来独立 RFC
各散点各自演进,不做统一即现状——键/失效三套口径,正是 #293 指出的缺口本身
测试回路改进程内 runner属 RFC-036 隔离模型的裁决面,本 RFC 不越界

实现策略 ​

依赖关系 ​

  • 前置:RFC-029 编排器(已稳定)、RFC-009a 证明管道(BFS 缓存宿主);
  • 并行:#247 use 追踪发现——增量重编译的依赖图准确性依赖它,未落地前 R2 脏判定 保守取全量;
  • 消费方:#290/#292 借用检查实现。

分波与风险 ​

  1. 第一波(L1):validate_source 收编三消费方 + FunctionCheckSession + CacheStats 最小版。风险低——纯内部重构,全 miss 行为不变;
  2. 第二波(L2):CompileSession + std 嵌入单次编译。风险中——编排器状态化, 需 #232/#243-245 全量回归;
  3. 第三波(L3 + 增量):磁盘缓存(std 优先)+ incremental_scheduler + DocumentCache 迁移。风险高——跨进程产物正确性完全依赖版本守卫。

开放问题 ​

  • [ ] L3 产物是否纳入类型环境序列化(决定 check 类消费者能否命中 L3,还是仅 run 可命中)
  • [ ] config_fingerprint 的语义位清单:哪些配置项改变编译产物语义
  • [ ] DocumentCache 迁移随第三波走,还是独立小步先行
  • [ ] 命中率归因口径:按调用计数还是按去重键计数(影响 metrics 的可解释性)

非目标 ​

  • 函数级增量解析(RFC-017 既有裁决:全文件解析仅几毫秒,不值得增量化);
  • JIT / 运行时缓存(RFC-028 管辖);
  • 推翻测试回路的子进程隔离(RFC-036 §6 设计选择;常驻服务方案见替代方案,另立 RFC);
  • 跨包失效传播(RFC-029 边界外);
  • 进程 spawn 成本优化(非缓存议题)。

附录B:设计决策记录 ​

决策决定日期记录人
宿主RFC-029 预留的 029a 行,不新建顶层 RFC2026-09-07晨煦
分层模型L1 进程内 / L2 会话模块级 / L3 磁盘2026-09-07晨煦
收益定位主战场 = 多入口复用与 std 链消除2026-09-07晨煦
子进程隔离不推翻L3 是子进程模型唯一跨进程收益通道2026-09-07晨煦
L3 不做跨模块失效传播版本守卫整键失效2026-09-07晨煦
度量强制无 metrics 不许合入2026-09-07晨煦

参考文献 ​

  • #293(本 RFC 的 issue);#251 P0-6 审计(借用 BFS 无缓存发现)
  • RFC-029 §核心原则(边界声明)与 §子 RFC 规划(029a 行)
  • RFC-009a(借用证明管道;BFS 缓存设计句)
  • RFC-017 §2(LSP DocumentCache 与文件级简化裁决)
  • RFC-014 §全局缓存(磁盘目录);RFC-028 §代码缓存(运行时层划界)