# JSIR: an MLIR-based JavaScript IR built to go back to source

> JSIR is Google's next-generation JavaScript analysis tooling, built on an MLIR-based high-level intermediate representation that supports both dataflow analysis and lossless conversion back to source. The design goal is the interesting part: an IR high-level enough to lift back to an AST and low-level enough for taint analysis, with MLIR regions used to model control flow.

**google/jsir** — Next-generation JavaScript analysis tooling

- Repository: https://github.com/google/jsir
- Stars: 673 · Forks: 33
- Language: C++
- License: Apache-2.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/google-jsir

## An IR that has to satisfy two opposite requirements

The design section is the part of the README that explains why JSIR exists at all, and it frames the problem as two goals that look incompatible.

The IR needs to be high-level enough to be lifted back to the AST, in order to support source-to-source transformation and decompilation. And it needs to be low-level enough to facilitate dataflow analysis, in order to support taint analysis, constant propagation and similar work.

Those two requirements pull in opposite directions. The AST is a tree of language constructs, which is exactly what you want if you plan to print the program back out, and exactly what you do not want if you plan to reason about where a value flows. A dataflow analysis wants a graph of basic blocks with explicit edges, which is exactly what you do not want if you intend to reconstruct an if statement from it.

The resolution is that JSIR defines a high level IR that uses MLIR regions to accurately model control flow structures. A region in MLIR is a block of operations with its own scope, and nesting regions inside operations is how MLIR represents structured control flow. So the control flow in JSIR is not flattened into a jump graph, it is nested, which is what lets the same representation be both analysable and printable.

The details of that design are in docs/intermediate_representation_design.md, and the README points there rather than duplicating it. The primary language of the repository is C++, and the tree shows the shape: a maldoca/ directory holds the bulk of the work, a third_party/ directory, a bazel/ directory, and BUILD, MODULE.bazel and WORKSPACE files at the root alongside a .bazelversion.

## Decompiling Hermes bytecode, and deobfuscation with an LLM

The use cases section describes where JSIR is used at Google, and the two examples are the two capabilities the design serves.

The first is decompilation. JSIR is used for decompiling Hermes bytecode all the way to JavaScript code, by using its ability to be fully lifted back to source code. Hermes is the JavaScript engine used by React Native, so this is bytecode that real applications ship, and getting from it back to readable JavaScript is the kind of problem where a faithful, structured IR is the hard part.

The second is deobfuscation, which uses the source-to-source transformation capability. Obfuscated JavaScript has to be transformed and then printed, and if the IR is lossy in any direction the transformation cannot be expressed.

The deobfuscation work is where the repository connects to a current research direction. The README points to a paper on combining the Gemini LLM and JSIR for deobfuscation, and separately to a talk at LLVM Developers' Meeting 2024 called Adversarial JavaScript Analysis with MLIR, with the slides and the recording linked. The talk title is worth pausing on, because adversarial suggests the analysis is aimed at code that is trying to resist analysis, which is the hardest version of the problem.

The CASCADE paper, described as an LLM-Powered JavaScript Deobfuscator at Google, is the more specific of the two artefacts: a system where a language model drives the transformations and JSIR provides the representation the model reasons about and emits. That combination only works because the IR is lossless in the direction the model needs, which is the whole design premise stated as a research result.

## Docker is the recommended path, and the image is a two-stage build

The getting started section leads with Docker and calls it the easiest way to get started:

```shell
# Build the Docker image
docker build -t jsir:latest .

# Run jsir_gen
docker run --rm jsir:latest jsir_gen --help

# Analyze a JavaScript file
docker run --rm -v $(pwd):/workspace jsir:latest jsir_gen --input_file=/workspace/yourfile.js
```

The third command is the one you will use most. It bind-mounts the current directory into the container at /workspace and points the tool at a file inside it, which means the same command works for any JavaScript file you have checked out locally.

The Dockerfile behind it is a two-stage build on ubuntu:24.04. The builder stage installs build-essential, clang, git, curl, python3, python3-pip, nodejs and npm, then installs Bazelisk through npm, copies the source into /workspace and runs bazelisk build //maldoca/js/ir:jsir_gen. The runtime stage is much smaller, with just libstdc++6 and python3, and it copies the built binary from the builder into /usr/local/bin/jsir_gen along with the conversion test examples into /examples.

That split matters for a reason that is easy to miss. The tool that gets shipped is a single statically linked binary that does not need the compiler, the build system or the LLVM source in the image. So while the build is heavy, the resulting container is small and quick to start, and the runtime image does not carry the LLVM development files that the builder needed.

The top-level requirements.txt in the repository is worth a glance, because it is nearly empty. It is autogenerated by pip-compile with Python 3.11 and contains a single package, tqdm, because the Python side of the project is minimal.

## The Bazel build pulls in LLVM, and that is the cost

The manual build path is documented in detail, and the warning about storage is the first thing to read.

The build tools section says the project has only tested clang on Linux, with the installation shown as apt update followed by apt install clang. The build system is Bazel, and the recommendation is to use Bazelisk to manage Bazel versions, installed through npm as the @bazel/bazelisk package.

Then the note that matters: the build takes a lot of storage space, and if you run out of space, Bazel will return a cryptic error. That is a well-known Bazel behaviour, and the warning exists because people hit it.

The reason is stated next: LLVM takes a long time to fetch and build, and the way to test that LLVM is properly included is to build a part of it:

```shell
# This will fetch LLVM and build its support library:

bazelisk build @llvm-project//llvm:Support
```

Building the whole project is then a matter of choosing the granularity:

```shell
# Build everything:
bazelisk build //...

# Or, build a single target:
bazelisk build //maldoca/js/ir:jsir_gen

# Or, build all targets in a directory:
bazelisk build //maldoca/js/ir/...
```

The tests work the same way, with bazelisk test //... for everything, a specific test such as //maldoca/js/quickjs:quickjs_test, or a directory such as //maldoca/js/ir/conversion/...

The target names tell you something about the structure. The quickjs test target suggests a QuickJS-based path exists in the tree, and the conversion directory under maldoca/js/ir is where the JavaScript-to-JSIR and back conversions are tested, which is the direction the whole design is built around.

## jsir_gen and the pass pipeline

The tool itself is called jsir_gen, and the running section shows how it is invoked:

```shell
bazelisk run //maldoca/js/ir:jsir_gen --\
    --input_file=$(pwd)/maldoca/js/ir/conversion/tests/if_statement/input.js \
    --passes=source2ast,ast2jsir
```

The passes argument is the heart of it. The pipeline is named rather than implied, so source2ast converts the source into an AST, and ast2jsir converts that AST into the JSIR representation. Passes are composable and named, which is the MLIR way of doing things, and it means the path from input to output is explicit rather than hidden inside the tool.

The example input is a file named input.js inside an if_statement test directory, which is a good hint about how the conversion is tested: a directory per construct, with the input and the expected output side by side. That is how you would test a lossless round trip, and the presence of the conversion tests in the Docker image suggests they double as examples.

What the tool does with the IR after that is not spelled out in the README, and that is a real gap for anyone wanting to use it. The pass list implies further stages are possible, since you would expect to be able to print back to source, and the design section talks about lifting back to the AST, but the exact pass name for the reverse direction is not given here. The documentation directory and the design document in docs/ are where to look for that.

The naming of the tool also suggests the intended workflow. A generator that takes a file and produces an IR file, with named passes and a file input, is built for batch use in a pipeline rather than for interactive exploration.

## What you get and what you do not

There is no official Google product here, and the README says so in a DISCLAIMER at the bottom. The repository is published under the Apache-2.0 licence, publishes no GitHub releases, and the last push was on 2026-10-13, so it is being worked on, but there is no version to pin.

The tree is small enough to read. There is a docs/ directory, a bazel/ directory, third_party/, a BUILD file, MODULE.bazel with its lock file, a WORKSPACE file, a .bazelrc and a .bazelversion, a Dockerfile, a CONTRIBUTING.md and a requirements.in alongside the compiled requirements.txt. The bulk of the code is under maldoca/, which is where the JavaScript front end, the IR dialect and the conversions live.

Two practical limits are worth naming. The first is the build. Depending on LLVM through Bazel means a large download, a long first build and a lot of disk, and the toolchain is tested only on Linux with clang. Anyone on macOS or Windows is going to be working in the container or in a Linux environment, and the Docker path is not a convenience here, it is the supported path.

The second is the maturity of the surface. JSIR is described as a next-generation tool, the use cases are internal to Google, and the public documentation is thin. There is a link to the design document, a link to two papers and a talk, and a set of build instructions. There is not a documented API for building your own pass, a stability statement about the dialect, or a compatibility policy for the IR. So the realistic adoption path is to read the design document, study how the existing passes are structured, and expect to spend time reading the source.

## Conclusion

Adopt JSIR if you are working on JavaScript analysis where you need both taint-style dataflow reasoning and a representation that can be printed back as source, since that combination is what this IR is designed around and what plain AST tooling does not give you. Do not adopt it expecting a stable public library, because the repository publishes no releases, carries a disclaimer that it is not an official Google product, and its own build depends on fetching and building LLVM. Before you commit, build the Docker image rather than fighting the Bazel and LLVM setup, read docs/intermediate_representation_design.md for the region-based control flow model, and check the MLIR dialect documentation for the operations your analysis needs, because the dialect surface is where the practical limits live.

## FAQ

### What is JSIR and what is it built on?

JSIR is a next-generation JavaScript analysis tool built around an MLIR-based high-level intermediate representation that supports both dataflow analysis and lossless conversion back to source. It uses MLIR regions to model control flow structures accurately, and the primary language of the repository is C++.

### What is JSIR used for at Google?

It is used for code analysis and transformation, specifically for decompiling Hermes bytecode all the way back to JavaScript, and for deobfuscating JavaScript through source-to-source transformation. There is also published work on combining JSIR with the Gemini LLM for deobfuscation.

### How do I build JSIR?

The recommended route is Docker: docker build -t jsir:latest . then run the jsir_gen binary from the image. The manual route needs clang on Linux, Bazelisk for the Bazel build, and building the LLVM support library first as a check, since the build takes a lot of storage space.

### What is the jsir_gen tool and how are passes specified?

jsir_gen converts a JavaScript source file into JSIR, and its passes argument names the pipeline stages explicitly, for example --passes=source2ast,ast2jsir. It is run with bazelisk run //maldoca/js/ir:jsir_gen and takes an --input_file argument.

### Is JSIR an official Google product?

No, the README carries a DISCLAIMER stating that this is not an official Google product. The repository is licensed under Apache-2.0, publishes no GitHub releases, and the last push was on 2026-10-13.

## Sources

- [google/jsir on GitHub](https://github.com/google/jsir)
- [Issues](https://github.com/google/jsir/issues)
- [License: Apache-2.0](https://github.com/google/jsir/blob/main/LICENSE)
- [README](https://github.com/google/jsir/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/google-jsir
