- 编译器
- 编程语言
- 开发工具
【免费下载链接】rescript-compiler
ReScript is a robustly typed language that compiles to efficient and human-readable JavaScript.
本文以 analysis/reanalyze/ARCHITECTURE.md 为核心骨架,结合 analysis/reanalyze 与 analysis/reactive 的源码实现,完整剖析 ReScript 官方死代码分析工具reanalyze的管道化架构。你将理解其 MAP→MERGE→SOLVE→REPORT 四阶段纯管道设计、前向不动点(fixpoint)活性分析算法、两遍可选参数分析,以及基于响应式集合(Reactive Collections)实现的增量式分析流水线与 glitch-free 调度语义,并能直接运用-reactive、-timing、-mermaid、-test-shuffle等标志复现文档中的架构视图与性能特性。
总览:四阶段纯管道设计
reanalyze 的死代码消除(DCE)分析被刻意设计为一条纯管道(pure pipeline),共分四个阶段:
- MAP:独立处理每一个
.cmt文件 → 产出每文件数据(per-file data) - MERGE:合并所有每文件数据 → 得到不可变的项目级视图(project-wide view)
- SOLVE:计算死代码/存活状态 → 得到携带问题列表(issues)的不可变结果
- REPORT:输出问题(唯一的副作用发生地)
源码出处:analysis/reanalyze/src/reanalyze.ml 中的
run_analysis函数正是按这四个阶段组织:process_cmt_files(MAP)→Merging计时段(MERGE)→Solving计时段(SOLVE)→Reporting计时段(REPORT)。
这一设计带来的四项核心能力:
- 顺序无关性(Order independence):无论以何种顺序处理文件,最终结果完全一致;
- 增量更新(Incremental updates):替换某一个文件的数据而无需重新处理其他文件;
- 可测试性(Testability):每个阶段都是纯函数,可独立构造输入、独立验证输出;
- 并行化潜力(Parallelization potential):阶段 1–3 均基于不可变数据,天然支持并行执行。
上述设计动机在 DEADCODE_REFACTOR_PLAN.md 中被进一步阐明:重构前旧架构因全局可变状态导致"无法增量分析、测试困难、无法并行、推理困难(依赖顺序的隐式变更)"四大问题,而纯管道重构的目标正是"分析是输入→结果的纯函数、消除全局可变状态、副作用只存在于边缘、不同处理顺序结果相同、增量分析成为可能"。
下图(来自文档附图)展示了批处理模式下的完整管道:
对应 Mermaid 源文件为 batch-pipeline.mmd,可用于自行修改与重新渲染。
关键数据结构:不可变数据模型
管道各阶段之间通过不可变数据类型传递数据。文档给出的核心类型表如下:
| 类型 | 用途 | 可变性 |
|---|---|---|
DceFileProcessing.file_data | 每文件采集到的数据 | Builders(AST 遍历期间可变) |
FileAnnotations.t | 源码注解(@dead、@live) | 合并后不可变 |
Declarations.t | 所有导出的声明(pos →Decl.t) | 合并后不可变 |
References.t | 值/类型引用(source → targets) | 合并后不可变 |
FileDeps.t | 跨文件依赖(file →FileSet.t) | 合并后不可变 |
OptionalArgsState.t | 计算得到的每声明可选参数状态 | 不可变 |
AnalysisResult.t | 求解器输出,内含Issue.t列表 | 不可变 |
DceConfig.t | 分析配置 | 不可变(显式传递) |
从 dce_file_processing.ml 的源码可以看到file_data的实际定义,它携带五个 builder,等待下游合并:
type file_data = { annotations: File_annotations.builder; (* @dead / @live 注解 *) decls: Declarations.builder; (* 导出的值/类型/异常声明 *) refs: References.builder; (* 对其他声明的引用 *) cross_file: Cross_file_items.builder; (* 需要跨文件解析的条目(可选参数、异常) *) file_deps: File_deps.builder; (* 本文件依赖了哪些文件 *) }这种"Builder(可变,仅在 MAP 阶段使用)→ 不可变类型(MERGE 之后使用)"的分界,是"局部可变、全局不可变"原则的直接体现。
Phase 1:MAP —— 每文件独立处理
入口函数:DceFileProcessing.process_cmt_file(analysis/reanalyze/src/dce_file_processing.ml)
输入:.cmt文件路径 +DceConfig.t
输出:包含以下 builder 的file_data:
annotations—— 来自源码的@dead、@live注解;decls—— 导出的值/类型/异常声明;refs—— 对其他声明的引用;file_deps—— 该文件依赖了哪些文件;cross_file—— 需要跨文件解析的条目(可选参数、异常)。
关键性质:此阶段允许局部可变状态(出于性能考虑),每个文件被完全独立地处理。process_cmt_file会依据cmt_infos.cmt_annots区分接口(Interface)与实现(Implementation):接口直接处理签名;实现则先检查是否存在对应的.cmti文件(影响@genType注解的采集策略),再依次执行Collect_annotations.structure、process_signature与Dead_value.process_structure完成三类信息的收集。
在管道编排层,reanalyze.ml 的load_cmt_file先通过Cmt_format.read_cmt读取编译产物,再依据exclude_paths前缀过滤、判定接口/实现与模块名,最后分发到 DCE(process_cmt_file)、异常分析(Exception.process_cmt)与终止性分析(Arnold.process_cmt)三条支线;process_cmt_files则负责收集全部.cmt/.cmti路径(自动跳过node_modules、_esy目录,或读取 rewatch 生成的.sourcedirs.json扫描计划以支持 monorepo)。
Phase 2:MERGE —— 合并 builder 为项目级视图
入口函数:Reanalyze.runAnalysis的 merge 段(analysis/reanalyze/src/reanalyze.ml)
输入:file_data list
输出:不可变的项目级数据结构。
核心操作(文档原文):
let annotations = FileAnnotations.merge_all (file_data_list |> List.map (fun fd -> fd.annotations)) let decls = Declarations.merge_all (file_data_list |> List.map (fun fd -> fd.decls)) let refs = References.merge_all (file_data_list |> List.map (fun fd -> fd.refs)) let file_deps = FileDeps.merge_all (file_data_list |> List.map (fun fd -> fd.file_deps))关键性质:所有合并操作都是可交换的(commutative)——file_data_list的顺序不影响合并结果,这正是顺序无关性的底层保证。
需要说明的是,当前源码中该阶段的具体实现已演化为更精细的"store"体系(见Reanalyze.runAnalysis的Merging计时段):非响应式路径下调用Declarations.merge_all、File_annotations.merge_all、Cross_file_items.merge_all后分别包装进Declaration_store、Annotation_store、Cross_file_items_store;引用图则通过References.create_builder+merge_into_builder增量累积,再依次执行Dead_type.process_type_label_dependencies(类型标签依赖解析)与Cross_file_items.process_exception_refs(跨文件异常引用解析),最后References.freeze_builder冻结为不可变引用图并包装进Reference_store。可见文档中的四条merge_all是逻辑骨架,而源码在保持同一"合并+冻结"语义的前提下提供了更完整的实现细节。
Phase 3:SOLVE —— 死活性计算
入口函数:DeadCommon.solveDead(非响应式)以及Reanalyze.runAnalysis中的可选参数第二遍扫描;响应式路径则为Reactive_solver.collect_issues(analysis/reanalyze/src/reactive_solver.mli)。
输入:全部合并后的数据 + 配置
输出:包含Issue.t list的AnalysisResult.t
算法:前向不动点 + 活性感知(liveness-aware)的可选参数分析。
核心活性计算(Liveness.compute_forward)
实现在 analysis/reanalyze/src/liveness.ml,分三步:
- 识别根(roots):带有
@live/@genType注解的声明,或在任何声明之外被外部引用的声明(is_root同时检查Annotation_store.is_annotated_gentype_or_live与externally_referenced表); - 构建索引:把每个声明映射到其出边引用(
refs_from方向)。源码中build_decl_refs_index先按文件对声明分组,再用pos_in_decl判断每条引用的posFrom落在哪个声明的区间内,从而把"表达式位置 → 目标"的引用规约成"声明 → 目标"的声明级依赖图,避免主循环中出现 O(worklist × refs) 的退化开销; - 前向不动点:从根出发沿引用不断传播活性,直到工作队列为空,返回全部存活位置集合。
live_reason类型区分了三种存活原因,便于-debug时诊断:
type live_reason = | Annotated (* 带 @live 或 @genType 注解 *) | ExternalRef (* 在任意声明之外被引用 *) | Propagated (* 被其他存活声明引用 *)传播循环中还有一个重要细节:若某个位置被注解为@dead,则不从它继续向外传播(is_annotated_dead检查),即死声明不会"连累"其下游。
Pass 1:死活性判定
- 通过前向传播计算活性;
- 对每个声明检查是否在存活集合中;
- 标记死声明并收集问题。
Pass 2:活性感知的可选参数分析
- 用 Pass 1 的结果构造
is_live谓词(源码中为Decl.is_live/Reactive_solver.is_pos_live); - 通过
CrossFileItems.compute_optional_args_state计算可选参数状态,并过滤掉来自死代码的调用; - 只对存活声明收集可选参数问题(例如"参数 X 从未被使用");
- 将可选参数问题合并进最终结果。
这种两遍设计确保可选参数告警只统计存活代码中的调用,避免当某函数仅被死代码调用时产生误报。
关键性质:纯函数 —— 不可变数据进、不可变数据出,无副作用。在响应式路径中,可选参数分析仍未完全接入响应式管道(文档注明其仍走非响应式路径,耗时约 8–14ms),并留有 TODO:把live_decls + cross_file_items → optional_args_issues加入响应式流水线。
Phase 4:REPORT —— 边缘输出
入口函数:Reanalyze.runAnalysis的 report 段
输入:AnalysisResult.t
输出:日志 / JSON 到 stdout
核心操作(文档原文):
AnalysisResult.get_issues analysis_result |> List.iter (fun issue -> Log_.warning ~loc:issue.loc issue.description)关键性质:所有副作用都集中在管道边缘。求解器从不直接打日志——Log_(analysis/reanalyze/src/log_.ml)是 REPORT 阶段专属的输出模块。若指定-json,则由Emit_json输出结构化结果;exception/termination分析的结果也在此阶段统一报告。
增量更新的架构保证
文档明确给出了"文件变更 → 增量更新"的标准路径:
- 仅对变更的文件重新执行 Phase 1 → 新的
file_data; - 在(以文件名为键的)
file_data映射中替换该项; - 重新执行 Phase 2(合并)—— 快速、纯函数;
- 重新执行 Phase 3(求解)—— 快速、纯函数。
核心洞见:不可变数据结构使得安全的增量更新成为可能——你可以只替换一个文件的数据,而不会影响其他文件的数据。这也正是-reactive模式与 reanalyze-server 长驻服务得以成立的根本前提。
响应式流水线(Reactive Pipelines)
响应式层(analysis/reactive)提供基于 delta 的增量更新:不再重跑整个阶段,变更会通过派生的集合自动传播。
核心响应式原语
| 原语 | 描述 |
|---|---|
Reactive.t ('k, 'v) | 通用响应式集合接口 |
subscribe | 注册 delta 通知 |
iter | 遍历当前条目 |
get | 按键查找 |
delta | 变更通知:Set (k, v)、Remove k或Batch [(k, v option); ...] |
source | 创建带 emit 函数的可变源集合 |
flatMap | 变换集合,可选合并同键值 |
join | 两个集合的哈希连接(左连接语义) |
union | 合并两个集合,可选合并同键值 |
fixpoint | 传递闭包:init + edges → reachable |
ReactiveFileCollection | 带变更检测的文件支持集合 |
这些接口在 analysis/reactive/src/reactive.mli 中均有完整签名,其中delta类型定义为:
type ('k, 'v) delta = | Set of 'k * 'v | Remove of 'k | Batch of ('k * 'v option) list (* (key, Some v) = set;(key, None) = remove *)基于拓扑调度的 Glitch-Free 语义
响应式系统采用accumulate-then-propagate(先累积、后传播)调度器实现glitch-free(无毛刺)传播,保证派生集合始终看到一致的父状态。工作机制:
- 每个节点有一个
level(拓扑序):- 源集合
level = 0; - 派生集合
level = max(父节点 levels) + 1;
- 源集合
- 每个组合子先把收到的 delta 累积到待处理缓冲(pending buffers)中;
- 调度器按 level 顺序访问脏节点并调用其
process(); - 每个节点在每一波(wave)中只处理一次,且能拿到所有父节点的完整输入。
典型排序示例(文档原文):
file_collection (L0) → file_data (L1) → decls (L2) → live (L14) → dead_decls (L15)当一批文件变更到达时:delta 先进入待处理缓冲(不立即处理)→ 调度器依次处理 level 0、level 1……→ 一个join只有在两个父节点都已更新后才会处理。这种设计从构造上消除了多层依赖带来的毛刺问题——analysis/reactive/test/glitch_free_test.ml 专门验证了反连接不会看到"refs 已更新而 decls 未更新"之类的部分状态。
Reactive.Registry与Reactive.Scheduler模块还提供:
- 带统计追踪的命名节点(用
-timing查看统计); to_mermaid()—— 生成管道图(用-mermaid标志);print_stats()—— 展示每节点耗时与 delta 计数。
全响应式分析流水线
响应式管道直接从源文件计算问题,且在缓存命中时零重算(文档原文图):
Files → file_data → decls, annotations, refs → live (fixpoint) → dead/live_decls → issues → REPORT ↓ ↓ ↓ ↓ ↓ ↓ ReactiveFile ReactiveMerge ReactiveLiveness ReactiveSolver iter Collection (flatMap) (fixpoint) (multiple joins) (only)关键性质:当没有文件变更时,不执行任何计算——所有响应式集合保持稳定,只有最后的collect_issues调用迭代预计算好的集合(O(issues))。
文档给出的全响应式管道图(约 25 个节点的高层视图):
Mermaid 源文件为 reactive-pipeline.mmd;完整版(44 个节点,由-mermaid标志自动生成)见 reactive-pipeline-full.mmd。README 还说明:仓库检入的是-no-transitive变体,因为跨文件hasRefBelow抑制在该模式下才生效,响应式失效 bug 最容易在此暴露。
流水线阶段
| 阶段 | 输入 | 输出 | 组合子 |
|---|---|---|---|
| 文件处理 | .cmt文件 | file_data | ReactiveFileCollection |
| 合并 | file_data | decls、annotations、refs | flatMap |
| 活性 | refs、annotations | live(位置集合) | fixpoint |
| 死/活划分 | decls、live | dead_decls、live_decls | join(按活性划分) |
| 死模块 | dead_decls、live_decls | dead_modules | flatMap+join(反连接) |
| 按文件分组 | dead_decls、refs | dead_decls_by_file、refs_by_file | 带 merge 的flatMap |
| 按文件问题 | dead_decls_by_file、annotations | issues_by_file | flatMap(排序+过滤+生成) |
| 错误 @dead | live_decls、annotations | incorrect_dead_decls | join(存活且带 @dead 注解) |
| 模块问题 | dead_modules、issues_by_file | dead_module_issues | flatMap+join |
| 报告 | 所有问题集合 | stdout | iter(仅迭代) |
ReactiveSolver 集合
| 集合 | 类型 | 描述 |
|---|---|---|
dead_decls | (pos, Decl.t) | 不在存活集合中的声明 |
live_decls | (pos, Decl.t) | 在存活集合中的声明 |
dead_modules | (Name.t, Location.t) | 仅含死声明的模块(反连接) |
dead_decls_by_file | (file, Decl.t list) | 按文件分组的死声明 |
value_refs_from_by_file | (file, (pos, PosSet.t) list) | 按源文件分组的引用(用于 hasRefBelow) |
issues_by_file | (file, Issue.t list * Name.t list) | 每文件问题 + 已报告的模块 |
incorrect_dead_decls | (pos, Decl.t) | 存活但带@dead注解的声明 |
dead_module_issues | (Name.t, Issue.t) | 模块问题(dead_modules 与 modules_with_reported 的连接) |
注:可选参数分析(未使用/冗余参数)尚未接入响应式管道,仍走非响应式路径(约 8–14ms)。TODO:将live_decls + cross_file_items → optional_args_issues加入响应式流水线。
Delta 传播
当某个文件变更时:
ReactiveFileCollection检测到变更,为file_data发出 delta;ReactiveMerge收到 delta,更新decls、refs、annotations;ReactiveLiveness收到 delta,通过增量不动点更新live集合;ReactiveSolver收到 delta,通过响应式 join 更新dead_decls和issues;- 只有受影响的条目被重算——未触及的条目保持稳定。
当没有任何文件变更时:
- 零计算——所有响应式集合保持稳定;
- 只有
collect_issues迭代(O(issues))——这是整条管道中唯一的迭代; - 报告开销与问题数量呈线性关系。
对应图示的 Mermaid/SVG 源见 delta-propagation.mmd 与 delta-propagation.svg。
性能特征
| 场景 | 求解 | 报告 | 总计 |
|---|---|---|---|
| 冷启动(4900 个文件) | ~2ms | ~3ms | ~7.7s |
| 缓存命中(0 个文件变更) | ~1-5ms | ~3-8ms | ~30ms |
| 单文件变更 | O(affected_decls) | O(issues) | 极小 |
关键洞见:缓存命中时,"求解"时间仅仅是迭代响应式issues集合的开销——没有 join 被重算,没有不动点被重跑,响应式集合保持稳定。需要说明的是,上表数字来自文档自述的基准测量(对应tests/analysis_tests/tests-reanalyze/deadcode-benchmark基准项目),README 中亦有实测对比:标准模式 CMT 处理 0.78s/总 1.01s,而响应式(热缓存)CMT 处理 0.01s/总 0.20s,约 5 倍总加速、74 倍 CMT 处理加速。数字会随机型与项目规模浮动,建议以make benchmark、make time-cache、make time-reactive(见 analysis/reanalyze/README.md)自行复测。
响应式模块划分
| 模块 | 职责 |
|---|---|
Reactive | 核心原语:source、flatMap、join、union、fixpoint、Scheduler、Registry |
ReactiveFileCollection | 带变更检测的文件支持集合 |
ReactiveAnalysis | 带文件缓存的 CMT 处理 |
ReactiveMerge | 从 file_data 派生 decls、annotations、refs |
ReactiveTypeDeps | 类型标签依赖解析 |
ReactiveExceptionRefs | 通过 join 解析异常引用 |
ReactiveDeclRefs | 把声明映射到其出向引用 |
ReactiveLiveness | 通过响应式不动点计算存活位置 |
ReactiveSolver | 通过响应式 join 计算 dead_decls 与 issues |
各模块接口可分别查阅 reactive_merge.mli、reactive_liveness.mli、reactive_solver.mli 等;响应式库本身的设计与用法见 analysis/reactive/README.md。
统计追踪(-timing)
使用-timing标志可查看每个节点的统计:
| 统计项 | 描述 |
|---|---|
d_recv | 收到的 delta 消息数(Set/Remove/Batch) |
e_recv | 收到的条目数(批量展开后) |
+in/-in | 从上游收到的增/删操作 |
d_emit | 向下游发出的 delta 数 |
e_emit | 发出的 delta 中的条目数 |
+out/-out | 发出的增/删操作(非零-out表示 churn/抖动) |
runs | 节点process()被调用的次数 |
time_ms | 累计处理时间 |
在Reanalyze.run_analysis_and_report中,Reactive.set_debug !Cli.timing把调度器调试输出复用-timing开关(避免与极冗长的-debug混淆),并依次打印Reactive_liveness.print_stats、Reactive_solver.print_stats与全局Reactive.print_stats。-mermaid则将当前管道图以 Mermaid 文本输出到 stderr——README 给出了用它重新生成 reactive-pipeline-full.mmd 的完整命令:
# 在任意 ReScript 项目中运行(-config 生效),捕获 stderr: rescript-tools reanalyze -config -reactive -no-transitive -mermaid \ >/dev/null 2> analysis/reanalyze/diagrams/reactive-pipeline-full.mmd测试策略:顺序无关性验证与分阶段单元测试
顺序无关性测试:使用仅用于测试的-test-shuffle标志随机化文件处理顺序。其实现位于 reanalyze.ml 的shuffle_list(Fisher–Yates 洗牌算法):当Cli.test_shuffle为真时,dce_data_list在合并前被随机重排,从而验证"结果不依赖遍历顺序"。对应的端到端脚本 test-order-independence.sh 会先跑一次基线(不洗牌),再连续 3 次用-test-shuffle运行,逐一diff输出,任何不一致即失败。
执行方式:
# 运行 reanalyze 相关测试 make test-reanalyze # 运行顺序无关性测试 make test-reanalyze-order-independence(顶层 Makefile 中test-reanalyze委托给tests/analysis_tests/tests-reanalyze/deadcode;test-reanalyze-order-independence目标由 tests/analysis_tests/tests-reanalyze/Makefile 逐级下放。)
分阶段单元测试:每个阶段可独立测试:
- Phase 1:处理单个
.cmt文件,验证file_data; - Phase 2:合并已知 builders,验证合并结果;
- Phase 3:以已知输入调用求解器,验证问题列表。
响应式库自身的组合子测试见 analysis/reactive/test:flat_map_test.ml、join_test.ml、union_test.ml、fixpoint_basic_test.ml、fixpoint_incremental_test.ml、batch_test.ml、glitch_free_test.ml、integration_test.ml分别覆盖各组合子、批量处理、无毛刺调度与端到端文件处理。
关键模块总览
| 模块 | 职责 |
|---|---|
Reanalyze | 入口,编排整条管道(reanalyze.ml) |
DceFileProcessing | Phase 1:每文件 AST 处理(dce_file_processing.ml) |
DceConfig | 配置(CLI 标志 + 运行配置) |
DeadCommon | Phase 3:求解器(solveDead、solveDeadReactive,dead_common.ml) |
Liveness | 前向不动点活性计算(liveness.ml) |
Declarations | 声明存储(builder/immutable) |
References | 引用追踪(source → targets) |
FileAnnotations | 源码注解追踪 |
FileDeps | 跨文件依赖图 |
CrossFileItems | 跨文件可选参数与异常 |
AnalysisResult | 不可变的求解器输出 |
Issue | 问题类型定义 |
Log_ | Phase 4:日志输出 |
ReactiveSolver | 响应式 dead_decls → issues 计算(reactive_solver.mli) |
结语
reanalyze 的架构精髓在于一条清晰的主线:MAP/MERGE/SOLVE/REPORT 四阶段纯管道保证了顺序无关、可增量、可测试、可并行;而响应式集合层则把"增量更新"从愿景落成实现——通过flatMap/join/union/fixpoint组合子、拓扑排序的 accumulate-then-propagate 调度器与不可变数据模型,使得缓存命中时全管道零重算、单文件变更时只重算受影响条目。理解这套架构,不仅可以直接上手rescript-tools reanalyze -config -reactive -timing -mermaid等命令观察管道运行,也为在 ReScript 生态中设计增量分析工具(如 reanalyze-server、编辑器集成)提供了可复用的范式。
- 编译器
- 编程语言
- 开发工具
【免费下载链接】rescript-compiler
ReScript is a robustly typed language that compiles to efficient and human-readable JavaScript.
相关推荐
SQLFluff 架构深度解析:从模板引擎到规则修复的四阶段流水线
SQLFluff 架构深度解析:从模板引擎到规则修复的四阶段流水线 SQLFluff 是一款模块化的 SQL 静态检查器(linter)与自动格式化工具,支持多
代码质量Lint格式化静态分析开发工具blink.cmp 架构深度解析:Trigger → Sources → Fuzzy → Render 四阶段补全流水线
blink.cmp 架构深度解析:Trigger → Sources → Fuzzy → Render 四阶段补全流水线 导读 blink.cmp 是一个面向
开发工具SQLFluff 架构解析:从模板渲染到自动修复的四阶段流水线
SQLFluff 架构解析:从模板渲染到自动修复的四阶段流水线 SQLFluff 是一个模块化的 SQL 代码检查(Linter)与自动格式化(Auto for
代码质量Lint格式化静态分析开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考