Rewriting a production compiler's IR with AI agents in five weeks Chromia's Rell language compiler was rewritten in five weeks using AI agents, a task that would have taken most of a year by hand, according to a developer who led the effort. The rewrite replaced a tree-walking interpreter with a Truffle-based backend on GraalVM to improve runtime performance, preserving Rell's unique relational query syntax that compiles to SQL. Every language that survives long enough ends up rewriting the guts of its compiler. C did it with Roslyn. Rust grew MIR. Kotlin spent years on the K2 transition, one I participated in from inside JetBrains' Kotlin team, where I remember the new JVM backend still calling into the old backend for the hardest parts, because nobody dared rewrite the inliner. The pattern is always the same: a language is born with prototype-quality internals, and then requirements arrive — a second backend, real IDE support, serialization — that the original trees were never designed to carry. For big languages, this rewrite is a multi-year, multi-team effort. This spring I did that rewrite for Rell https://gitlab.com/chromaway/rell , Chromia's language for building decentralized applications dapps , in five weeks, directing AI agents. This post is about the decision that shaped it, which was mine to make and mine to live with, and about the part I would not have attempted without them. Rell is a standalone statically typed language with Kotlin-like syntax, whose relational operations compile to SQL. The following runs in the playground https://chromiaproject.github.io/rell-playground/ 's SQL dry-run pane: entity user { key name: text; mutable age: integer; } entity post { key id: integer; index author: user; body: text; } // Each @-expression compiles to its own SELECT. query main { // Filter + sort + projection. val adults = user @ { .age = 18 } @sort .name, .age ; // Aggregate: count per author GROUP BY . val post counts = post @ {} @group .author.name, @sum 1 ; return adults = adults, post counts = post counts ; } Those are @-expressions, Rell's query syntax. The two in main come out as: select A00."name", A00."age" from "c0.user" A00 where A00."age" = ? order by A00."name", A00."rowid" select A01."name", COALESCE SUM ? ,0 from "c0.post" A00 join "c0.user" A01 on A00."author" = A01."rowid" group by A01."name" order by A01."name" Note what the second one did: .author.name walked an entity reference, and the join fell out of the compiler. This is the part of Rell with no counterpart on other chains. A contract on Ethereum, Solana or the Move chains gets key-value storage, and anything resembling a query is somebody else's off-chain problem. Rell's state is a relational database, and querying it is language syntax: type-checked against the schema at compile time, compiled to SQL, executed inside the consensus boundary. I have maintained Rell solo since February 2026; real blockchain networks run on every release. Rell is a niche language, with a smaller blast radius than Rust or Kotlin, and much simpler than either. I would still have budgeted most of a year for this project by hand: untangling the old trees node by node, rewriting the interpreter, keeping both worlds running mid-migration. That database is bought, not free: the fastest chains compute orders of magnitude faster than Chromia, and Rell pays for its query layer in throughput. The same kind of trade runs through language implementations. LLVM spends a lot of compile time and emits fast code, which is the deal Julia takes: the first call to a function pays for compiling it, and the code that comes out runs at C speed. CPython spends none and executes slowly. The JVM, V8 and .NET sit in between, compiling as they go. A tree-walking interpreter, which is what Rell had, sits at the CPython end: it starts instantly and then runs about as slowly as you would expect from walking a tree per operation. That is the gap Truffle closes, and it is why the second backend was worth a rewrite. Truffle is a framework on GraalVM an extended Java VM that takes an interpreter written to its conventions and lets the JIT compiler specialize it to the program being run. The alternative for the same speedup is emitting JVM bytecode from the compiler, which is a much larger and more delicate thing to own. I wanted the backend built on Truffle, and the compiler's output model could not support one. It also could not support something I wanted more: serialization. Before the rewrite, the compiler's output model R App was mutable, lazy, full of compiler-internal sentinels. Every node carried its own execution, as an abstract evaluate frame on R Expr . SQL generation machinery SqlGenContext , SqlBuilder lived inside the model package, on the same classes the compiler built. @-expressions mixed IR nodes with runtime evaluator classes in one file. The runtime was tied to the compiler the way it is in every language still running on its original internals: not by a bad decision, but by a thousand convenient ones. The consequences: no second backend execution was hard-wired into one tree walk , no serialization you cannot serialize behavior , and every node in a blockchain network re-parses and re-compiles every app from source, forever. This is where most of my effort went: not typing, one question. Two options. The conservative option is the K2 move I remembered from JetBrains: build a new IR, and let it call into the old execution code in the hard places. It is the rational choice for a team that cannot afford to rewrite everything, and it is how large migrations actually ship. It is also a compromise you live with for years. The radical option: make the IR pure data no behavior on nodes at all and re-assemble every scattered piece of interpretation as new, exhaustive matching over the new tree. Including the database semantics: @-expressions, SQL generation, create/update/delete. I chose the radical option, and the serialization goal is what settled it. A node that delegates to compiler-side code has nothing to write into a language-neutral binary format: the delegation is JVM code, and code is exactly what the format cannot carry. The hybrid was not just worse; it had no representation. A hard external requirement is worth a lot here: it converts a taste argument into a constraint. The shape that came out: after all compiler passes, one resolution step that, quoting the architecture doc shipped with it, "forces every lazy field, drops compiler and IDE baggage, and replaces live object references with integer indices into flat arrays," producing an immutable, self-contained IR that serializes to FlatBuffers a binary serialization format and is the only thing the runtime consumes. In the codebase it is called the RR tree. php flowchart LR SRC source files -- "ANTLR4" -- S "S AST " S -- C "C compilation,