# Calyx: an intermediate language for hardware accelerator generators

> Calyx is an intermediate language and a compiler infrastructure for people who write generators of custom hardware. The idea is to take a program that describes a computation, put it through a front end into a small well specified intermediate representation, run passes over that representation, and emit Verilog or a circuit description from it. The implementation is Rust for the compiler and Python for the surrounding tooling.

**calyxir/calyx** — Intermediate Language (IL) for Hardware Accelerator Generators

- Repository: https://github.com/calyxir/calyx
- Website: https://calyxir.org
- Stars: 615 · Forks: 75
- Language: Rust
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/calyxir-calyx

## What the intermediate language is for

Calyx is an intermediate language and infrastructure for building compilers that generate custom hardware accelerators.

The subject is worth unpacking, because the term covers two different things. There is the language itself, a well specified intermediate representation that sits between a compiler frontend and a hardware backend. And there is the infrastructure: the passes, the library, the driver, and the tools that a compiler project needs in order to be more than a single file that prints Verilog.

The design is based on a paper presented at a computer architecture conference in 2021, and the readme links to it. That is the right way to think about the project: it is a research artefact that has been made into infrastructure, and the paper is the specification of the ideas while the repository is the implementation of them.

The architecture follows the usual compiler shape, with one unusual feature. In a conventional compiler the intermediate representation is an implementation detail: you write a frontend and a backend and the representation in between is yours to change. In Calyx the representation is the product. It is published as a separate crate with its own version, it is documented on its own site, and other people write passes for it. So a user of Calyx might write a frontend that produces Calyx, and a backend that consumes it, and never touch the internals of either.

That is what makes it an intermediate language rather than a compiler. The language is a contract between independent pieces of software, and the infrastructure exists so that the contract can be used.

The scope is hardware accelerators, which means the kind of computation the representation is designed for is a structured dataflow pipeline: components with well defined interfaces, wired together, with the control and the data paths described explicitly. That is a very different design space from general purpose computing, and the representation is shaped by it.

## The five crates, and using the compiler as a library

The organisation section is a list of five crates, and the list is the clearest statement of the compiler's architecture.

The first is a utilities crate, holding the shared helpers. The second is the frontend, which is the parser and the abstract syntax tree for the source language. The third is the crate for the intermediate language itself, the representation and everything that operates on it. The fourth holds the optimisations for that representation. And the fifth is the compiler driver, the binary that ties the stages together.

The separation between the intermediate language and its optimisations is the part that matters, and it is what makes the last item in the list possible.

You can use the Calyx compiler as a library and implement your own optimisations, and the readme points at an example provided by the optimisations crate to show how. The install is a single command.

```bash
cargo install calyx
```

That installs the compiler driver as a binary that can optimise and compile Calyx programs to Verilog or to a circuit description. So the intended workflow for somebody with a new transformation is: write it as a pass against the representation, plug it into the pipeline, and get the benefit of the existing front end, the existing back ends, and the existing driver without writing any of them.

That is the difference between an intermediate language and a library with a nice API. A library gives you functions you can call. An intermediate language gives you a representation other tools agree on, and once several projects agree on a representation, the tools compose in ways that were never designed in advance.

Each of the five crates is published separately, and the readme links each one on the crate registry, which is a small thing that says the boundary is real: these are independent pieces with their own versions, not directories in one binary.

The driver is the user-facing part, and the readme describes it as installing a binary that can optimise and compile Calyx programs to Verilog or to a circuit description from the circuit compiler toolchain. The two back ends matter for a practical reason: Verilog is what goes into a vendor's toolchain, and the circuit description is what goes into an open source toolchain and into other compiler flows.

## The Rust workspace, and the version discipline

The workspace manifest is long, and it is worth reading for the shape of the project rather than for the individual entries.

The members fall into groups. The core Calyx crates are the ones the readme listed: the representation, the utilities, the optimisations, the frontend, and two more, a standard library and a backend. Then there is a group named for a different tool, a debugging server with an index and a debug adapter protocol implementation, which suggests the project has a remote debugging story. Then the new build driver, with its core. Then a miscellaneous tools group: a data conversion tool, a pass explorer, a group of auxiliary tools with their own sub-crates for a numeric representation and for formats, a component cells tool, a file info emitter, a writer, a data generator, and a petal tool. Then a language server, and finally a playground crate in the web directory.

Two things in that list are worth pausing on. A pass explorer is the tool you use to see what a pass did to the representation, and a language server is what makes the language usable in an editor with completion and diagnostics. Both are the work that turns a research language into something a student can use in the first hour, and both are the kind of thing a research project tends to skip.

The workspace also excludes a directory explicitly, which is a sign of a monorepo that has absorbed something it does not own.

The shared package metadata is where the versioning policy lives. The version is 0.7.1, the language edition is a recent one, and there is a minimum Rust version declared. The internal crates are all declared together with a path and the same version, which is how a monorepo keeps five crates in lockstep, and the external dependencies are listed with a comment explaining the convention: when adding a new dependency, do not pin the minor version, use the short form instead of the full one.

That comment is small but it is the kind of instruction that only gets written after somebody has hit the problem, because a minor version pin in one crate and not another is a class of bug that is very hard to find.

The rest of the repository supports the Rust work: a formatter configuration, a toolchain file pinning the compiler, a change log, a lock file, a directory of benchmarks, a directory of tests, a directory of documentation, and a book configuration for the documentation site.

## The Python side, and the two comments that explain it

The Python configuration is the newest part of the repository and it is doing more than it looks.

The project file declares a package with a name, a version, and a dependency on the AST crate from the Rust side. Then the interesting part: a list of sources that all point at workspace members rather than at published versions. The Python packages for the pipeline front end, the systolic language front end, the matrix expression language front end, the queues front end, the build driver, the report parser, and the profiler are all members of one workspace, so a local checkout builds all of them together.

That is a monorepo that spans two languages, which is much harder than it sounds. The Cargo workspace and the Python workspace have to agree about what exists, and the Python side has to consume the Rust side as a library rather than by shelling out to the binary.

Two comments in that file explain the current state honestly, and they are worth quoting rather than paraphrasing.

The first is on the profiler dependency group, and it says the profiler should probably be added to the workplace but the author does not want to deal with that right now. The second is on the scripts group, and it explains that some of the new build driver components import pieces of the original build driver's Python implementation, that it is a medium term goal to split that functionality out so it is independent, and that the old package can then be removed.

Both comments are the same shape: a known piece of technical debt, with a plan and a reason for the delay. That is much more useful to a reader than a clean dependency list, because it tells you that the new tooling is not yet independent of the old, and that somebody knows it.

The third comment is about tooling consistency rather than code. The linter configuration pins an exact version, and the comment says it is to use the same linter version across multiple Python linting tasks, for both pushes and pull requests.

That is the sort of thing that sounds trivial and is not. If one continuous integration task runs a different linter version than another, the two report different diagnostics for the same file, and a contributor cannot tell whether a failure is a real problem or a version difference.

The optional dependency groups mirror the front ends, so a user installs only the extras for the front end they actually want, and the whole Python side is managed with the fast package manager and a lock file.

## The container, and four tools built from source

The Dockerfile is a sequence of build stages for external tools, and it is a good picture of what a hardware generator project actually depends on.

The first stage builds a Scala project, the Dahlia compiler, using a prebuilt image for the Scala build tool, and it checks out a specific commit rather than a tag. The comment explains why the fetch comes before the checkout, which is that the clone might be cached and a checkout of a specific commit needs the objects to be present.

The second stage builds Icarus Verilog, the open source Verilog simulator, from a Debian base with the usual build tools installed, using an autoconf script, a configure step, and make.

The third stage builds the tensor compiler, with a comment from one of the maintainers saying that he cannot find a way to shove this install into its own image, and another comment marking a piece of hard coding as a to-do. The clone there pulls the entire history of the main branch rather than a single tag, and the comment says explicitly that this is not ideal.

The fourth stage builds Verilator, another Verilog tool, with the same pattern and the same comment about the final install happening later.

The pattern across all four is consistent and instructive. The tools are built from source rather than installed from packages, which is what a research project does when the published package lags behind what the project needs. The comments are candid about the costs: the stages are large, the final install is deferred to a later stage, and one of the clones is heavier than it needs to be.

None of that is criticism of the project, it is the reality of depending on four tools that have to be at specific versions. And the comments are the useful part, because a reader who wants to reproduce the environment can see exactly which shortcuts were taken and why.

The rest of the repository's tooling supports the same goal. There is a dev container configuration, a Docker ignore file, a hooks directory with a setup script, a runner configuration, a Python linting configuration, and a directory for the web playground, whose Rust crate is a workspace member.

## The examples, and the tools around the language

The examples directory is short and its names are a curriculum.

There is a tutorial, which is where a newcomer starts. There is a directory named after the futil utility language,. There is a directory for the Dahlia front end, whose name also appears in the container build. And there is a directory for a synchronisation example, which is the hard case in hardware, because synchronising concurrent blocks is where a generator either works or does not.

Four examples, chosen to cover the progression from simple to difficult, is a much better selection than a dozen that do the same thing.

The rest of the repository is the set of tools that surround a language, and the directory names describe the shape of the project. There is a language server, so the language has editor support. There are Python bindings, so the tooling can be scripted. There is the debugging server with its index and its debug adapter implementation, so a frontend can step through a generated circuit. There is a primitives directory, which is the component library a user builds on. There are the two build drivers, the original and the newer one. There is a directory for the web playground. And there are two directories whose names are unexplained by the readme, which are likely internal packages.

The documentation setup is also worth noting. There is a book configuration, which is the tool that builds a documentation site from Markdown, and the readme links the language documentation, the source documentation, and the project website as three separate destinations. The source documentation is generated from the published crates, which means the API reference cannot drift from the code.

And the final piece of the picture is the licence and the version. The project is MIT licensed, the version is 0.7.1 with the last tagged release in early 2024, and the repository itself is still being pushed to. The gap between the last release and the recent commits is the usual sign of a research project that moves on its own schedule rather than a publishing one, and for a user it means that the repository, not the published crate, is where the current work is.

## Conclusion

Calyx fits a research group writing a compiler for an accelerator, and a student learning how a hardware generator is put together, since the passes and the intermediate representation are the parts a paper describes and the driver is the part you can read. It does not fit somebody who wants to generate Verilog from a high level language for its own sake, because the intermediate representation is the product and it is not the easiest thing to target. Verify first that the version of the language you are reading about matches the version of the compiler, since the language and the passes have both changed, that the back ends you need are available in the tool chain you install, and that you can tolerate a research project where the language server, the Python bindings, and the build infrastructure are all moving at once.

## FAQ

### What is the Calyx project?

It is an intermediate language and compiler infrastructure for building compilers that generate custom hardware accelerators. A program is parsed into a well specified intermediate representation, passes run over that representation, and the result is compiled to Verilog or to a circuit description. The design comes from a paper presented in 2021.

### How is the Calyx compiler split into crates?

Into a utilities crate, a frontend with the parser and the abstract syntax tree, a crate for the intermediate language itself, a crate holding the optimisations, and the compiler driver. Each is published separately on the crate registry, and the separation is what lets somebody write their own pass and plug it into the existing pipeline.

### Can I write my own Calyx optimisation?

Yes. The readme says you can use the compiler as a library and implement your own optimisations, and it points at an example provided by the optimisations crate. The intended workflow is to write a pass against the representation and reuse the existing frontend, back ends, and driver.

### Why does the Calyx repository build four external tools from source?

The container has stages for the Dahlia compiler, the Icarus Verilog simulator, the tensor compiler, and Verilator, each built rather than installed from packages because the published versions lag behind what the project needs. The comments in the file are candid about the costs, including that one clone pulls the entire history of a branch because a lighter fetch was not worked out.

### What is the Python side of the Calyx repository for?

It holds the front ends for the pipeline and systolic languages, the matrix expression language, the queues front end, the report parser, and the profiler, all as members of one Python workspace that consumes the Rust AST crate as a library. Two comments in the configuration acknowledge that the newer build driver still imports parts of the older one and that the profiler is not yet fully integrated.

## Sources

- [calyxir/calyx on GitHub](https://github.com/calyxir/calyx)
- [License: MIT](https://github.com/calyxir/calyx/blob/main/LICENSE)
- [Project website](https://calyxir.org)
- [README](https://github.com/calyxir/calyx/blob/main/README.md)
- [Releases](https://github.com/calyxir/calyx/releases)

---

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