Jaehun Baek

Qwerty: Modernizing a Quantum Compiler (Part 1)

September 11, 2025

This summer, I had the incredible opportunity to work as an Undergraduate Research Assistant through the Georgia Tech Open Source Program Office's Virtual Summer Internship Program. The program pairs students with exciting open-source projects, and I was matched with

Qwerty, a high-level quantum programming language designed to make quantum programming more intuitive by abstracting away low-level circuit details. My mission for the summer was ambitious: to help modernize Qwerty's compiler by replacing a key component and diving headfirst into the world of modern compiler architecture.

This article is the first in a series where I'll document my journey—the challenges I faced, the concepts I learned, and the contributions I made.

Overhauling the Compiler's Classical Path

Every compiler's job is to translate human-readable source code into machine-executable instructions. In Qwerty, this process involves handling both quantum and classical computations. The project's primary goal was to modernize how the compiler handles the classical parts of a Qwerty program. Previously, the compiler relied on an external C++ library called Tweedledum to synthesize classical circuits. The workflow looked like this:

  • Old Flow: Qwerty classical AST → Tweedledum Library → Synthesized Circuit

While functional, this approach had its limits. Relying on an external library made the compiler less modular and harder to optimize. The summer project aimed to replace this with a modern, integrated pipeline using MLIR, a powerful compiler infrastructure from the LLVM project.

The new, improved flow would be:

  • New Flow: New Flow: Qwerty classical AST → Ccirc Dialect (MLIR) → Optimizations → LLVM IR

This change would bring the entire process inside our compiler, giving us more control and opening the door for new optimizations. To get there, however, we first needed to understand the tools of the trade: LLVM and MLIR.

  • LLVM is a foundational set of reusable compiler and toolchain technologies. It provides the low-level "backend" for many of the world's most popular compilers.
  • MLIR (Multi-Level Intermediate Representation) is a framework built on top of LLVM to make building new compilers easier. It's like a toolkit for creating custom languages, or "dialects,"that can represent complex program logic. For us, using MLIR was an engineering convenience that would allow us to define our own classical circuit logic and have it integrate seamlessly with the rest of the LLVM ecosystem.

Milestone 1: LLVM Upgrade

Before we could build anything new, we had to get the house in order. The first milestone was a foundational one: upgrading Qwerty's LLVM dependency from version 19.1.6 to a more recent stable release, 20.1.6.

This was more than just changing a version number in a file. It was a deep dive into the codebase to find and resolve any compatibility issues. I spent the initial weeks building LLVM locally on my machine to create a testing environment. Then, the real work began. I relied heavily on a few powerful Git commands to perform a sweeping find-and-replace across the entire project:

  • git grep: This command became my best friend, allowing me to search the entire repository for every instance of the old version number and other deprecated code.
  • git sed: A command I set up with a custom alias to perform in-place text replacement, which was invaluable for updating documentation and build scripts efficiently.
This process taught me not just about the compiler's dependencies, but also about the practical, powerful ways developers manage large-scale changes in a complex codebase.

Learning the Language of Compilers

While upgrading LLVM, I was also on a steep learning curve, absorbing the core concepts that make compilers tick. My weekly notes from this period are filled with new terminology and ideas.

An Intermediate Representation (IR), I learned, is the language a compiler uses internally to represent code after it has been parsed but before it's converted to machine code. MLIR is a framework for creating these IRs.

Learning the Language of Compilers

Upgrading LLVM wasn't just a mechanical task of find-and-replace; it was a deep-dive immersion into the very heart of compiler engineering. To work on a project of this scale, I had to learn the tools and concepts that developers use to manage complexity and squeeze out every drop of performance.


The Build System: CMake and Ninja

Before you can even compile a compiler, you have to... compile the compiler. A massive project like LLVM is composed of millions of lines of code across thousands of files. Building it is a complex process managed by a build system.

During my work, I became familiar with two key tools: CMake and Ninja. They work together as a team:

  • CMake is a "meta-build system." It doesn't compile the code itself. Instead, it reads a set of configuration files (CMakeLists.txt) and generates the actual build files tailored for your specific operating system and environment.
  • Ninja is a small, efficient build system that is obsessed with speed. CMake generates Ninja build files, and then you run Ninja to execute the build. It's incredibly fast at figuring out which files need to be recompiled, making the development cycle much quicker.

Mastering this workflow was essential for building LLVM locally on my machine, which was a necessary step to test and verify the version upgrade.

Intermediate Representation

I quickly learned that compilers don't work directly with the source code we write. They first convert it into an Intermediate Representation (IR). The IR is the compiler's native language, a structured format that's easier to analyze and transform than raw source code.

In the eyes of the compiler, every piece of the IR—every instruction, every variable type—is a distinct 'object'. This structured view is what allows the compiler to perform powerful analyses and optimizations on the code.

Dataflow Analysis: Understanding the Code's Journey

One of the most powerful analyses a compiler performs is called dataflow analysis. The best analogy I learned is to think of it like tracing variables in your head as you read a function. The compiler does the same thing, but with mathematical rigor.

For example, if the compiler sees this code:

x = 5;
y = x + 2;
z = 7;

Through dataflow analysis, the compiler can determine that y will always be 7. This allows it to perform optimizations like "constant propagation," replacing y with 7 everywhere else in the program, making the code smaller and faster.

A Lesson in Performance: LLVM's Custom Type Casting

In a compiler, you are constantly working with different kinds of IR "objects." You might have a pointer to an object and need to know, "Is this an addition instruction? Is it a qubit type? Is it something else?"

In standard C++, this is often handled by a feature called Run-Time Type Information (RTTI), using tools like dynamic_cast. However, RTTI can be slow, as it often involves looking up information in metadata tables. For a high-performance project like LLVM, that overhead is unacceptable. So, the LLVM developers built their own, faster system for type casting.

Here are the key tools I learned to use:

  • llvm::isa<Type>(obj): This is the "is a" check. It returns true or false. It's perfect for conditional logic. For example: if (llvm::isa<qcirc::QubitType>(some_value)) { ... } checks if some_value is a qubit type.
  • llvm::cast<Type>(obj): This is the "assertive" cast. It assumes you already know the object is the correct type and performs a fast conversion. If you're wrong, the program crashes. It's used when you are certain of the type, for maximum speed.
  • llvm::dyn_cast<Type>(obj): This is the safe, dynamic cast. It checks the type and, if it matches, returns a valid pointer. If it doesn't match, it returns a null pointer. This is the most common and safest choice when you're not 100% certain of an object's type.

This attention to detail—creating a custom, high-speed type-checking system—was a perfect illustration of the performance-first mindset that permeates compiler development. It showed me that in this field, even the smallest details matter.