☰
ReScript reanalyze 死代码分析架构深度解析:从四阶段纯管道到响应式增量流水线
2026/9/28 2:55:26 网站建设 项目流程
  • 编译器
  • 编程语言
  • 开发工具

【免费下载链接】rescript-compiler

ReScript is a robustly typed language that compiles to efficient and human-readable JavaScript.

项目地址:https://gitcode.com/gh_mirrors/re/rescript-compiler
点击查看免费下载

本文以 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),共分四个阶段:

  1. MAP:独立处理每一个.cmt文件 → 产出每文件数据(per-file data)
  2. MERGE:合并所有每文件数据 → 得到不可变的项目级视图(project-wide view)
  3. SOLVE:计算死代码/存活状态 → 得到携带问题列表(issues)的不可变结果
  4. 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,分三步:

  1. 识别根(roots):带有@live/@genType注解的声明,或在任何声明之外被外部引用的声明(is_root同时检查Annotation_store.is_annotated_gentype_or_live与externally_referenced表);
  2. 构建索引:把每个声明映射到其出边引用(refs_from方向)。源码中build_decl_refs_index先按文件对声明分组,再用pos_in_decl判断每条引用的posFrom落在哪个声明的区间内,从而把"表达式位置 → 目标"的引用规约成"声明 → 目标"的声明级依赖图,避免主循环中出现 O(worklist × refs) 的退化开销;
  3. 前向不动点:从根出发沿引用不断传播活性,直到工作队列为空,返回全部存活位置集合。

live_reason类型区分了三种存活原因,便于-debug时诊断:

type live_reason = | Annotated (* 带 @live 或 @genType 注解 *) | ExternalRef (* 在任意声明之外被引用 *) | Propagated (* 被其他存活声明引用 *)

传播循环中还有一个重要细节:若某个位置被注解为@dead,则不从它继续向外传播(is_annotated_dead检查),即死声明不会"连累"其下游。

Pass 1:死活性判定

  1. 通过前向传播计算活性;
  2. 对每个声明检查是否在存活集合中;
  3. 标记死声明并收集问题。

Pass 2:活性感知的可选参数分析

  1. 用 Pass 1 的结果构造is_live谓词(源码中为Decl.is_live/Reactive_solver.is_pos_live);
  2. 通过CrossFileItems.compute_optional_args_state计算可选参数状态,并过滤掉来自死代码的调用;
  3. 只对存活声明收集可选参数问题(例如"参数 X 从未被使用");
  4. 将可选参数问题合并进最终结果。

这种两遍设计确保可选参数告警只统计存活代码中的调用,避免当某函数仅被死代码调用时产生误报。

关键性质:纯函数 —— 不可变数据进、不可变数据出,无副作用。在响应式路径中,可选参数分析仍未完全接入响应式管道(文档注明其仍走非响应式路径,耗时约 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分析的结果也在此阶段统一报告。

增量更新的架构保证

文档明确给出了"文件变更 → 增量更新"的标准路径:

  1. 仅对变更的文件重新执行 Phase 1 → 新的file_data;
  2. 在(以文件名为键的)file_data映射中替换该项;
  3. 重新执行 Phase 2(合并)—— 快速、纯函数;
  4. 重新执行 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(无毛刺)传播,保证派生集合始终看到一致的父状态。工作机制:

  1. 每个节点有一个level(拓扑序):
    • 源集合level = 0;
    • 派生集合level = max(父节点 levels) + 1;
  2. 每个组合子先把收到的 delta 累积到待处理缓冲(pending buffers)中;
  3. 调度器按 level 顺序访问脏节点并调用其process();
  4. 每个节点在每一波(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_dataReactiveFileCollection
合并file_datadecls、annotations、refsflatMap
活性refs、annotationslive(位置集合)fixpoint
死/活划分decls、livedead_decls、live_declsjoin(按活性划分)
死模块dead_decls、live_declsdead_modulesflatMap+join(反连接)
按文件分组dead_decls、refsdead_decls_by_file、refs_by_file带 merge 的flatMap
按文件问题dead_decls_by_file、annotationsissues_by_fileflatMap(排序+过滤+生成)
错误 @deadlive_decls、annotationsincorrect_dead_declsjoin(存活且带 @dead 注解)
模块问题dead_modules、issues_by_filedead_module_issuesflatMap+join
报告所有问题集合stdoutiter(仅迭代)

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 传播

当某个文件变更时:

  1. ReactiveFileCollection检测到变更,为file_data发出 delta;
  2. ReactiveMerge收到 delta,更新decls、refs、annotations;
  3. ReactiveLiveness收到 delta,通过增量不动点更新live集合;
  4. ReactiveSolver收到 delta,通过响应式 join 更新dead_decls和issues;
  5. 只有受影响的条目被重算——未触及的条目保持稳定。

当没有任何文件变更时:

  • 零计算——所有响应式集合保持稳定;
  • 只有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)
DceFileProcessingPhase 1:每文件 AST 处理(dce_file_processing.ml)
DceConfig配置(CLI 标志 + 运行配置)
DeadCommonPhase 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.

项目地址:https://gitcode.com/gh_mirrors/re/rescript-compiler
点击查看免费下载

相关推荐

上一篇:Python Fire完全指南:10分钟掌握自动化CLI生成神器
下一篇:TodoMVC测试框架揭秘:如何确保45个实现版本的功能一致性

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询