JSIR: A High-Level IR for JavaScript

Google has introduced JSIR, a high-level intermediate representation for JavaScript built on MLIR, designed to enable advanced dataflow analysis and high-fidelity source-to-source transformations.
This RFC introduces JSIR, a high-level IR for JavaScript:
- JSIR preserves all information from the AST and supports high-fidelity round-trip between source ↔ AST ↔ JSIR;
- JSIR uses MLIR regions to represent control flow structures;
- JSIR supports dataflow analysis.
JSIR is developed and deployed in production at Google for code analysis and transform use cases.
JSIR is open source here: GitHub - google/jsir: Next-generation JavaScript analysis tooling · GitHub.
Motivation
Industry trend of building high-level language-specific IRs
The compiler industry is moving towards building high-level language-specific IRs. For example, the Rust and Swift compilers perform certain analyses on their high-level IRs before lowering down to LLVM. There are also a number of ongoing projects in this direction, such as Clang IR, Mojo, and Carbon.
The need for a high-level JavaScript IR
Why do we need a high-level IR for JavaScript specifically? While much of JavaScript tooling relies on ASTs (like ESTree), complex analyses require a control flow graph (CFG) and dataflow analysis capabilities, which JSIR provides by using the MLIR framework.
Source-to-source transformations
Many JavaScript tooling use cases require emitting JavaScript code as output.
For example:
Transpilation: Babel converts newer versions of JavaScript to older versions of JavaScript, to maximize browser compatibility.
Optimization:
Closure compiler optimizes JavaScript into shorter and faster JavaScript, to minimize download time and maximize performance.
Bundling: Webpack bundles multiple JavaScript files into a single JavaScript file.
These tools all need to operate on a representation that allows code generation back to JavaScript. As a result, they all operate on an AST.
There are tons of AST-based open-source tools, but no IR-based ones
The JavaScript community has a lot of AST-based open-source tools. For example, Babel provides, as its public APIs, an AST with traversal and scope utils; ESLint depends on espree which also provides an AST and relevant utils.
To get a sense of how many JavaScript ASTs there are, here is a list on AST Explorer:
There is even a standard for JavaScript ASTs - ESTree.
However, there is no tool that exposes an IR instead of an AST. JSIR seeks to fill this gap.
Use cases at Google
JSIR is used at Google for code analysis and transform use cases. For example:
Decompilation
JSIR is used for decompiling the Hermes bytecode all the way to JavaScript code, by utilizing its ability to be fully lifted back to source code.
Deobfuscation:
JSIR is used for deobfuscating JavaScript by utilizing its source-to-source transformation capability.
See our latest paper on how we combine the Gemini LLM and JSIR for deobfuscation. This paper has been accepted to ICSE 2026 SEIP track and will be presented on April 15, 2026.
JSIR design goals
A public and stable IR definition
JSIR seeks to fill a gap in the JavaScript community for a public, open-source IR-based tool. To achieve this goal, the definition of JSIR is public, stable, and comprehensive. In particular, it closely follows ESTree, to the extent that most, if not all, JSIR operations have 1-1 mappings from ESTree nodes.
Captures all source-level information
JSIR is not intended to be used for low-level optimization (for example, a JIT is expected to define lower-level IRs, even though they might be lowered from JSIR). Instead, JSIR is a high-level IR that represents all source-level information, in order to support use cases like source-to-source transformation and decompilation.
One design goal for JSIR is that we can convert JSIR back to the JavaScript AST perfectly. In other words, the following round-trip should be lossless:
Source ↔ AST ↔ JSIR
Easy to use
The key benefit of an IR over an AST is that we can perform dataflow analysis. Therefore, we need to expose a dataflow analysis framework (built on top of the MLIR dataflow analysis framework). Such framework must provide an easy way of defining lattices and transfer functions.
Other considerations:
- Can we provide an easy IR traversal util like @babel/traverse for AST?
- Can we manipulate the IR in JavaScript / TypeScript?
- Can we integrate JSIR into godbolt.org?
Why JSIR is interesting to the MLIR community
Battle-test MLIR functionalities
JSIR’s success would provide solid proof that MLIR is capable of defining IRs for general purpose languages. Currently, the core “IR definition” part has been proven to be effective, as demonstrated by JSIR and other projects like ClangIR and Mojo.
Now, we seek to battle-test more “advanced” MLIR functionalities. For example, we have made a wrapper dataflow analysis API on top of MLIR to provide ease-of-use improvements, and hope to contribute our learnings by upstreaming some of these improvements. We also seek to use and potentially improve symbol table, memory effects, etc.. All of these will make MLIR truly the go-to option for building any compiler in the future.
Use MLIR to “represent AST”
There have been discussions on whether MLIR can be used to represent the AST
(reference), and Mojo is pioneering the idea of parsing directly to MLIR.
JSIR is aiming at something even more extreme - an IR that can lift back to source. If JSIR is successful, then it really proves that MLIR can represent ASTs, and that the boundary between AST and IR is perhaps very blurry.
Eliminate the need for ASTs
If a high-level IR can preserve all information from an AST, then we can start to question whether we need ASTs at all.
The fact that Mojo and Carbon perform all analyses on IRs suggest that IRs already have all the analysis capabilities to replace ASTs. However, our experience has shown that developers (especially those unfamiliar with compilers) find ASTs much easier to understand and work with compared to IRs.
JSIR design highlights
NOTE: This section is taken from intermediate_representation_design.md in the repo.
A critical goal of JSIR is to ensure an accurate conversion of the IR back to the AST. Paired with Babel’s AST → source printer, this means we can lift the IR back to source. This “reversible” IR design enables source-to-source transformations - we perform IR transformations then lift the transformed IR to source.
Internal evaluations on billions of JavaScript samples showed that AST - IR round-trips achieved 99.9%+ success resulting in the same source.
In the following sections, we will describe important design decisions that achieve this high-fidelity round-trip.
Post-order traversal of AST
Let’s start from the simplest case - straight-line code, i.e. a list of statements with no control flow structures like if
-statements.
Each of these simple expression / statement AST nodes is mapped to a corresponding JSIR operation. Therefore, JSIR for straight-line code is equivalent to a post-order traversal dump of the AST.
For example, for the following JavaScript statements:
1 + 2 + 3;
4 * 5;
The corresponding AST is as follows (see astexplorer for the full AST):
[
ExpressionStatement {
expression: BinaryExpression {
op: '+',
left: BinaryExpression {
op: '+',
left: NumericLiteral { value: 1 },
right: NumericLiteral { value: 2 }
},
right: NumericLiteral { value: 3 }
}
},
ExpressionStatement {
expression: BinaryExpression {
op: '*',
left: NumericLiteral { value: 4 },
right: NumericLiteral { value: 5 }
}
},
]
The corresponding JSIR is as follows:
%1 = jsir.numeric_literal {1}
%2 = jsir.numeric_literal {2}
%1_plus_2 = jsir.binary_expression {'+'} (%1, %2)
%3 = jsir.numeric_literal {3}
%1_plus_2_plus_3 = jsir.binary_expression {'+'} (%1_plus_2, %3)
jsir.expression_statement (%1_plus_2_plus_3)
%4 = jsir.numeric_literal {4}
%5 = jsir.numeric_literal {5}
%4_mult_5 = jsir.binary_expression {'*'} (%4, %5)
jsir.expression_statement (%4_mult_5)
Source: Hacker News

















