在编译器技术的发展历程中,中间表示(Intermediate Representation,IR)一直是连接源码与目标代码的核心抽象。传统上,JavaScript 生态依赖抽象语法树(AST)进行代码分析与转换,然而 AST 缺乏控制流图(CFG)与数据流分析能力,这在复杂分析场景中成为瓶颈。Google 近期开源的 JSIR 正是为解决这一难题而生,它基于 MLIR 框架构建,旨在实现源码与 IR 之间的无损往返,为 JavaScript 工具链提供一种全新的 IR 选择。
JSIR 的设计动机与行业背景
现代编译器正在经历一场从单一低层 IR 向多层次 IR 架构演进的范式转变。Rust 与 Swift 编译器均在高层 IR 上完成特定分析后再降级到 LLVM IR;Clang IR、Mojo、Carbon 等项目也在探索语言特定高层 IR 的可能性。这一趋势的核心驱动力在于:不同层次的 IR 擅长不同类型的分析与优化,高层 IR 可以保留更多源码语义信息,便于进行源码到源码的转换与逆向工程。
具体到 JavaScript 领域,现有工具链高度依赖 AST 进行代码处理。Babel 用于将新版 JavaScript 转译为旧版本以最大化浏览器兼容性,Closure Compiler 负责将 JavaScript 优化为更短更快的版本,Webpack 则将多个 JavaScript 文件打包为单个文件。这些工具的共同特点是输入输出均为 JavaScript 代码,因此都建立在 AST 之上进行操作。然而,AST 缺乏 SSA 形式与控制流图的表达能力,无法直接进行跨函数的全局分析与优化。JSIR 的出现填补了这一空白,它在保留 AST 完整信息的同时,提供了 IR 级别的抽象能力。
核心设计目标与实现策略
JSIR 的首要设计目标是实现源码、AST 与 IR 之间的高保真往返。具体而言,从源码解析为 AST,再从 AST 转换为 JSIR,最后从 JSIR 完美还原为 AST,这一系列转换的成功率需要在百分之九十九以上。内部评估显示,在数十亿 JavaScript 样本上的往返成功率已达到百分之九十九点九以上。
为实现这一目标,JSIR 采用了几项关键设计策略。首先,JSIR 操作与 ESTree 节点之间存在近乎一对一映射关系。ESTree 是 JavaScript 社区的 AST 标准,大多数 JSIR 操作都可以直接对应到 ESTree 节点,这大大降低了从 AST 转换到 JSIR 的复杂度。其次,对于顺序执行的代码,JSIR 等价于 AST 的后序遍历结果。以表达式 1 + 2 + 3; 与 4 * 5; 为例,AST 首先被解析为嵌套的二叉表达式结构,而 JSIR 则将其展开为线性的 SSA 形式,每个中间结果都有明确的虚拟寄存器标识。这种表示方式既保留了原始表达式的结构信息,又引入了 IR 的分析能力。
在符号与值的设计上,JSIR 明确区分了左值与右值。以赋值表达式 a = b; 为例,左值 a 代表对某对象或内存位置的引用,右值 b 则代表具体的值。在 AST 中这两者以相同的方式表示(均为 Identifier 节点),但在 JSIR 中则采用不同的操作:jsir.identifier_ref 用于左值引用,jsir.identifier 用于右值读取。这种区分对于精确的代码生成与反编译场景至关重要。
控制流结构的区域化表示
JSIR 使用 MLIR 区域(Region)来表示控制流结构,这是其区别于传统低层 IR 的显著特征。对于每种控制流结构,JSIR 都定义了单独的操作(如 jshir.if_statement、jshir.while_statement),嵌套的代码块则作为区域嵌入其中。这种设计完全保留了原始控制流的层级结构,使得从 JSIR 还原回 AST 只需标准的递归遍历即可完成。
以 if 语句为例,条件表达式作为普通 SSA 值传入,而 then 分支与 else 分支则各自作为一个区域。这种表示方式清晰明确,不会丢失任何源码级别的结构信息。对于 while 循环则有所不同:由于条件在每次迭代时都需要重新求值,条件部分同样被表示为一个区域而非单一值。这种细微的差别体现了 JSIR 对 JavaScript 语义的理解深度。
逻辑表达式(如 a && b)的处理则展示了区域表示的更复杂应用。在 jshir.logical_expression 中,左操作数是 SSA 值,右操作数是区域。这是因为左操作数总是首先被求值,而右操作数仅在左操作数为真时才会被求值 —— 这正是短路求值的语义。区域机制使得这种条件执行的语义得以精确表达,同时保持了源码结构的可还原性。
数据流分析能力
JSIR 在 MLIR 原生数据流分析框架之上构建了专用的分析 API,并进行了多项易用性改进。核心改进包括:定义 JsirStateRef 类封装对分析状态的所有写入操作,使得依赖的 WorkItem 自动被推入工作列表,用户无需手动调用 propagateIfChanged;提供 JsirDataFlowAnalysis 与 JsirConditionalForwardDataFlowAnalysis 基类,统一了稀疏分析(附加在 Value 上)与密集分析(附加在 ProgramPoint 上)的实现;定义 JsirGeneralCfgEdge 结构统一了基本块之间与区域之间的分支,包括早期退出(break 与 continue 语句)等场景。
这些改进显著降低了数据流分析的使用门槛。与上游 MLIR API 相比,JSIR 用户无需为每个分析分别编写稀疏版本与密集版本,也无需在每次分析时都加载常量传播与死代码分析。这些工作由框架自动处理。
实际应用场景
JSIR 已在 Google 内部的生产环境中部署,主要应用于以下场景。反编译是 JSIR 的核心应用之一:Facebook 的 Hermes 字节码可以通过 JSIR 完全还原为 JavaScript 源码,这得益于 JSIR 精确保留源码语义的能力。去混淆是另一个重要场景,JSIR 与大语言模型结合用于 JavaScript 代码去混淆,相关论文已被 ICSE 2026 SEIP track 接收。这些真实案例证明了 JSIR 在工程上的可行性。
与传统方案的对比
传统的 JavaScript 代码分析通常采用两种路径:一是直接操作 AST,优点是语义清晰、与源码结构对应,缺点是缺乏全局分析与优化能力;二是将 JavaScript 降级到 LLVM IR 进行优化,这虽然可以获得强大的优化能力,但会丢失所有高层语言特性,且生成的代码无法再还原为 JavaScript。JSIR 处于这两者之间的独特位置:它保留了足够的高层信息以支持源码到源码的转换,同时引入了 IR 级别的抽象以支持数据流分析。
展望未来,JSIR 团队正在探索几个方向:采用 MLIR 内置的符号表功能替代当前的标识符表示;将数据流分析的易用性改进贡献回 MLIR 上游;以及考虑将 JSIR 纳入 MLIR 主干。然而也存在实际挑战:JSIR 依赖 QuickJS 进行常量折叠以避免重新实现 JavaScript 语义,同时也依赖 Babel 或 SWC 进行解析,将这些依赖引入 LLVM 仓库是否可行仍需社区讨论。
JSIR 的出现标志着 JavaScript 编译器技术进入了一个新阶段,它为工具链开发者提供了一种既保留源码语义又具备分析能力的中间表示选择。随着更多社区力量的参与与贡献,JSIR 有望成为 JavaScript 代码分析与转换领域的基础设施。
资料来源:LLVM Discourse 上的 JSIR RFC(https://discourse.llvm.org/t/rfc-jsir-a-high-level-ir-for-javascript/90456)