{"slug": "rewriting-a-production-compiler-s-ir-with-ai-agents-in-five-weeks", "title": "Rewriting a production compiler's IR with AI agents in five weeks", "summary": "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.", "body_md": "Every language that survives long enough ends up rewriting the guts of its\ncompiler. C# did it with Roslyn. Rust grew MIR. Kotlin spent years on the\nK2 transition, one I participated in from inside JetBrains' Kotlin team,\nwhere I remember the new JVM backend still calling into the *old* backend\nfor the hardest parts, because nobody dared rewrite the inliner. The\npattern is always the same: a language is born with prototype-quality\ninternals, and then requirements arrive — a second backend, real IDE\nsupport, serialization — that the original trees were never designed to\ncarry. For big languages, this rewrite is a multi-year, multi-team effort.\n\nThis spring I did that rewrite for\n[Rell](https://gitlab.com/chromaway/rell), Chromia's language for\nbuilding decentralized applications (dapps), in five weeks, directing\nAI agents. This post\nis about the decision that shaped it, which was mine to make and mine to\nlive with, and about the part I would not have attempted without them.\n\nRell is a standalone statically typed language with Kotlin-like syntax,\nwhose relational operations compile to SQL. The following runs in the\n[playground](https://chromiaproject.github.io/rell-playground/)'s SQL\ndry-run pane:\n\n```\nentity user {\n    key name: text;\n    mutable age: integer; \n}\n\nentity post {\n    key id: integer;\n    index author: user;\n    body: text;\n}\n\n// Each @-expression compiles to its own SELECT.\nquery main() {\n    // Filter + sort + projection.\n    val adults = user @* { .age >= 18 } ( @sort .name, .age );\n\n    // Aggregate: count per author (GROUP BY).\n    val post_counts = post @* {} ( @group .author.name, @sum 1 );\n\n    return (adults = adults, post_counts = post_counts);\n}\n```\n\nThose are @-expressions, Rell's query syntax. The two in\n`main()`\n\ncome out as:\n\n```\nselect A00.\"name\", A00.\"age\" from \"c0.user\" A00\n  where A00.\"age\" >= ? order by A00.\"name\", A00.\"rowid\"\n\nselect A01.\"name\", COALESCE(SUM(?),0) from \"c0.post\" A00\n  join \"c0.user\" A01 on A00.\"author\" = A01.\"rowid\"\n  group by A01.\"name\" order by A01.\"name\"\n```\n\nNote what the second one did: `.author.name`\n\nwalked an entity reference,\nand the join fell out of the compiler.\n\n**This is the part of Rell with no counterpart on other chains. A\ncontract on Ethereum, Solana or the Move chains gets key-value storage,\nand anything resembling a query is somebody else's off-chain problem.\nRell's state is a relational database, and querying it is language\nsyntax: type-checked against the schema at compile time, compiled to\nSQL, executed inside the consensus boundary.**\n\nI 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.\n\nThat 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.\n\nThat 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.\n\nBefore the rewrite, the compiler's output model (`R_App`\n\n) was mutable,\nlazy, full of compiler-internal sentinels. Every node carried its own\nexecution, as an abstract `evaluate(frame)`\n\non `R_Expr`\n\n. SQL generation\nmachinery (`SqlGenContext`\n\n, `SqlBuilder`\n\n) lived inside the *model*\npackage, on the same classes the compiler built. @-expressions mixed IR\nnodes with runtime evaluator classes in one file. The runtime was tied\nto the compiler the way it is in every language still running on its\noriginal internals: not by a bad decision, but by a thousand convenient\nones.\n\nThe 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.\n\nThis is where most of my effort went: not typing, one question. Two options.\n\nThe 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.\n\nThe 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.\n\nI 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.\n\nThe shape that came out: after all compiler passes, one resolution step\nthat, quoting the architecture doc shipped with it, \"forces every\nlazy field, drops compiler and IDE baggage, and replaces live object\nreferences with integer indices into flat arrays,\" producing an immutable,\nself-contained IR that serializes to FlatBuffers (a binary serialization\nformat) and is the *only* thing the runtime consumes. In the codebase it is called the RR tree.\n\n``` php\nflowchart LR\n    SRC[source files] -- \"ANTLR4\" --> S[\"S_ (AST)\"]\n    S --> C[\"C_ (compilation,<br/>13 passes)\"]\n    C --> R[\"R_ (compiler model:<br/>mutable, lazy, sentinels)\"]\n    R -- \"resolve()\" --> RR[\"RR_ (resolved IR:<br/>immutable, flat arrays)\"]\n    RR <-- FlatBuffers --> BIN[(\"serialized app\")]\n    RR --> INT[\"tree-walking interpreter\"]\n    RR --> TF[\"Truffle backend\"]\n```\n\nThe mechanics, briefly. IR nodes are sum types with a closed set of\nvariants — 39 expression, 17 statement, 16 database-expression — and the\ninterpreter is pattern matching over them that the compiler checks for\nexhaustiveness, with per-domain logic split into separate files. In the\nKotlin below, a `sealed interface`\n\nis a sum type whose variants are all\ndeclared in one file and known to the compiler, a `data class`\n\nis a\nrecord with structural equality, and `when (expr) { is X -> ... }`\n\nis\nthe match over them: leave a variant out and the code does not compile.\nThe same node before and after, lightly trimmed from the repo:\n\n```\n// before: execution lives on the compiler's node\nclass R_IfExpr(\n    type: R_Type,          // compiler type object, drags the compiler in\n    private val cond: R_Expr,\n    private val trueExpr: R_Expr,\n    private val falseExpr: R_Expr,\n): R_BaseExpr(type) {\n    override fun evaluate0(frame: Rt_CallFrame): Rt_Value {\n        val b = cond.evaluate(frame).asBoolean()\n        return (if (b) trueExpr else falseExpr).evaluate(frame)\n    }\n}\n\n// after: the node is data...\nsealed interface RR_Expr {\n    val type: RR_Type\n\n    data class If(\n        override val type: RR_Type,   // plain data, no compiler references\n        val cond: RR_Expr,\n        val trueExpr: RR_Expr,\n        val falseExpr: RR_Expr,\n    ): RR_Expr\n\n    // ...\n}\n\n// ...and the interpreter owns the behavior, one arm per variant\nfun evaluateExpr(expr: RR_Expr, frame: Rt_CallFrame): Rt_Value = when (expr) {\n    is RR_Expr.If -> {\n        val cond = (evaluateExpr(expr.cond, frame) as Rt_BooleanValue).value\n        evaluateExpr(if (cond) expr.trueExpr else expr.falseExpr, frame)\n    }\n    // ...38 more arms; the compiler rejects a missing one\n}\n```\n\nCross-references are integer indices into flat per-kind arrays (entities, structs, functions, queries…); serialized modules carry only index vectors, and convenience maps are rebuilt on deserialization. The resolver that builds all this is the only code allowed to force the compiler's lazy fields; after it runs, nothing touches compiler types again. The public compile entry point returns the RR tree rather than the compiler model, so the boundary is also the library's API surface.\n\nThe compiler/runtime boundary is enforced by the build system's module\n*dependency* graph (arrows read \"depends on\"):\n\n``` php\nflowchart BT\n    rrtree[\"rr-tree (IR data, 1.4K LOC)\"] --> utils\n    rrser[\"rr-serialization\"] --> rrtree\n    frontend[\"frontend (parser + compiler)\"] --> rrtree\n    frontend --> utils\n    rcore[\"runtime-core (values, types, SQL, stdlib)\"] --> frontend\n    rinterp[\"runtime-interpreter (5.4K LOC)\"] --> rcore\n    rtruffle[\"runtime-truffle (7.8K LOC)\"] --> rcore\n    rtruffle --> rinterp\n```\n\nTwo honest notes on that graph. `runtime-core`\n\nstill depends on the\nfrontend module (\"for now\", says the architecture doc), and most of that\ndependency is the standard library: stdlib functions are declared\nthrough the compiler's library framework, so their implementations still\nhandle compiler-model types. The execution modules are the clean ones;\nthe interpreter imports nothing from the frontend. The hard module split\nitself landed two weeks after the main commit. And `rr-serialization`\n\nhas *no* production consumer inside the repo: a client that only wants\nto load compiled programs needs `rr-tree`\n\nplus `rr-serialization`\n\nand\nnothing else: no parser, no compiler.\n\nThe main commit landed on April 21: 641 files, +32,050/−16,450. Rename detection tells the honest story of new versus moved. The value types, the runtime contexts and the database-driver plumbing are recognized as moves. The interpreter is not: about 4,700 lines of dispatch, including all the database semantics, written new over the RR tree. Around the main commit: deserialization hardening April 24, the GraalVM Truffle backend May 4 (+6,437), its specialization work May 8, test wiring May 21. About 17K lines of new Kotlin across three new modules.\n\nWhat the diff actually bought is the structure every compiler textbook\ndraws and few languages have before their first internals rewrite: a\nfrontend that turns source into a description of the algorithm, and a\nbackend that consumes that description to run it or to translate it\nfurther. Before, there was no such seam. `evaluate`\n\non the model class\nwas the backend. Now the frontend ends at `resolve()`\n\n, and everything\ndownstream, tree-walker, Truffle, a future consumer in another language,\nis a backend reading the same data.\n\nRewriting the database semantics is the part I would not have dared alone. With agents doing the mechanical half, daring became affordable. Without them, Rell would have gotten the new-IR-calls-old-code compromise.\n\nOne detail matters beyond this project. The work was done with Claude Opus 4.6, a model generation older and weaker than the agents that later rewrote Bun. The architectural rewrite did not wait for a stronger model. The limiting factor was the design and the verification, not the model generation.\n\nToday every node re-parses and re-compiles each Rell app from source. With serialized RR in the chain configuration (the shared settings every node of a network runs from), a node could execute compiled programs directly through a thin runtime: no parser, no frontend, less code and startup work per node. For that to be safe, executing the serialized form must be provably indistinguishable from compiling from source. On a blockchain, any difference is a consensus split: nodes compute different results and stop agreeing on the chain's state. That is what the round-trip invariant states:\n\n```\ninterpret(compile(x)) ≡ interpret(deserialize(serialize(compile(x))))\n```\n\nIt is enforced by re-running the full test suite (~3,700 tests) through a\nserialize/deserialize pass. Together with the Truffle backend this makes\na **three-backend differential**: the same suite runs under the\ntree-walking interpreter, the round-trip interpreter, and Truffle, and\nthe bar is that every test passes under all three.\n\n``` php\nflowchart LR\n    T[\"~3,700 tests<br/>+ real deployed apps' suites\"] --> A[\"tree-walking<br/>interpreter\"]\n    T --> B[\"serialize → deserialize<br/>→ interpreter\"]\n    T --> D[\"Truffle backend\"]\n    A --> EQ{\"all tests<br/>pass?\"}\n    B --> EQ\n    D --> EQ\n    EQ -- no --> BUG[\"it is a bug:<br/>on a chain, a consensus split\"]\n```\n\nThe Truffle pass runs in CI on GraalVM, with a guard test that exists only to prevent a silent fallback to the tree-walker from voiding the differential. The round-trip pass runs automatically in CI when a change touches the IR or the serialization layer, and can be triggered manually on any other pipeline.\n\nThe differential's job is not to warn that a design change is coming;\nthat part is usually obvious. It is to prove the change landed\neverywhere. In July, Rell gained value blocks (blocks usable as\nexpression arms, yielding a value) and jump expressions, and it was\nclear from the feature design that both backends would have to recognize\nthe new control flow. An expression-shaped `execute`\n\nhas no status slot\nto return, so control flow moved to stackless exception escapes,\nTruffle's conventional design. This reversed an earlier, equally\ndeliberate move to integer status codes, made when profiles of workloads\nwith many small calls showed exception handling dominating rule\nevaluation. The new features made an exception channel necessary anyway;\nquoting the design note in the source, \"one conventional mechanism for\nall control flow beats two coexisting ones.\" The flip was right. Its\nreach was one boundary short: the standard-library call boundary still\nhandled escapes the old way, so the new features worked in the\ntree-walking interpreter and crashed under Truffle, which has its own\ncontrol-flow exception family. Running the suite across backends\nsurfaced it at the desk, the boundary now converts between the two\nfamilies, and the regression tests for it run across all three backends.\nA single-backend suite would have left the mismatch for someone else to\nfind.\n\nThe other half of \"safe to execute serialized code\" is the deserialization security analysis, committed next to the schema. The question it answers: can a crafted, serialized program binary do more than Rell source code could? The answer is a mitigation table, not a promise — allocation caps, integer wrap-around checks, duplicate mount names (the names that map definitions to SQL tables) treated as a consensus-divergence risk, a recursion guard, and a build-time SHA-256 over the schema files, verified on every deserialize. A fuzz test runs 2,000 deterministic inputs plus byte-flip and truncation sweeps on every build; deserialization either succeeds or fails with a typed error, never a VM-fatal crash.\n\nOne honest caveat: thin-runtime nodes are not deployed. Shipping serialized RR into chain configurations has not been green-lit, so production still compiles from source. There is no hard technical barrier left: the serialization, the hardening and the invariant are built and tested ahead of that decision, which is the right order to build them in.\n\nThe Truffle backend ships behind an opt-in runtime flag and is loaded by class name at run time, so it sits in the distribution without being wired in: full test parity, deliberately not the default. It is a peer backend in the strict sense: same runtime values, same standard library, same database connection; only the dispatch differs. The repo's own rule for it is one sentence: \"Differences between the tree-walker and Truffle are Truffle bugs; the tree-walker is the canonical reference.\"\n\nThe reason a solo maintainer can own a JIT backend at all is what the\ncode looks like. Take the same `if`\n\nexpression from earlier. Emitting\nJVM bytecode for it, the classic way to make a JVM language fast, means\nwriting something in this register:\n\n```\n// the path not taken: hand-emitting bytecode\nLabel elseBranch = new Label(), end = new Label();\ncompile(cond, mv);                       // leaves a boolean on the stack\nmv.visitJumpInsn(IFEQ, elseBranch);\ncompile(trueExpr, mv);\nmv.visitJumpInsn(GOTO, end);\nmv.visitLabel(elseBranch);\nmv.visitFrame(F_SAME, 0, null, 0, null); // stack map, or the verifier rejects it\ncompile(falseExpr, mv);\nmv.visitLabel(end);\nmv.visitFrame(F_SAME, 0, null, 0, null);\n```\n\nYou are now maintaining stack maps, local-variable slots and verifier\nrules, and a mistake surfaces as a\n[ VerifyError](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/VerifyError.html)\nat class-load time\nrather than a wrong answer you can debug. The Truffle version of the\nsame node, trimmed from the repo, is the interpreter you would have\nwritten anyway:\n\n``` js\ninternal class Generic(\n    @field:Child private var cond: Tf_ExprNode,\n    @field:Child private var trueBranch: Tf_ExprNode,\n    @field:Child private var falseBranch: Tf_ExprNode,\n): Tf_IfExprNode() {\n    override fun execute(frame: VirtualFrame): Rt_Value =\n        if (cond.executeBoolean(frame)) trueBranch.execute(frame)\n        else falseBranch.execute(frame)\n}\n```\n\nOne JVM fact makes the rest of this section legible. The JVM splits\nvalues into primitives, which live in registers and on the stack, and\nobjects, which are allocated on the heap. An interpreter that hands\naround one generic value type boxes every intermediate result into an\nobject, so a loop that adds integers allocates on every iteration. That\nis what `executeBoolean`\n\nabove avoids: the typed path returns a raw\nboolean instead of wrapping it.\n\nThe\n[ @Child](https://www.graalvm.org/truffle/javadoc/com/oracle/truffle/api/nodes/Node.Child.html)\nannotations and the\n\n[are the whole contract: they tell Graal the tree shape is stable, so partial evaluation can compile this method against](https://www.graalvm.org/truffle/javadoc/com/oracle/truffle/api/frame/VirtualFrame.html)\n\n`VirtualFrame`\n\n*one*program's nodes and constant-fold the dispatch away. This is the first Futamura projection, done for real and in production: specialize an interpreter to a fixed program and what falls out is a compiler for it. What is left reads like the tree-walker. That is the deal Truffle offers: interpreter-shaped source, compiled-language speed, and the machinery that gets you there is not yours to maintain.\n\nWhat gets faster is running a Rell program, not compiling one: same compiler, same output, a different backend consuming it. How much faster depends on how much of the program's time went into walking the tree in the first place. Loops, recursion and branching are almost all dispatch, and that is what partial evaluation removes. Code that spends its time inside standard-library functions, or whose expression trees are shallow, has little dispatch to remove and barely moves. The baseline throughout is hand-written Kotlin: the speed of code compiled for the JVM directly.\n\nThe honest way to show the spread is the [per-commit benchmark report from\nCI](https://chromaway.gitlab.io/-/rell/-/jobs/15761948926/artifacts/public/report.html)\n(run with JMH, the standard JVM benchmark harness, on GraalVM 21; 73\nbenchmarks across 7 suites; a public CI artifact, not my laptop; that\njob artifact expires 30 days after the run, so a copy is [mirrored\nhere](/CommanderTvis/writing/blob/main/rr-truffle-rewrite/report.html)).\nThe harness matters because JVM code starts out interpreted and is\ncompiled only after the JIT has watched it run, so the first iterations\nof anything measure the wrong thing; JMH warms each benchmark up and\nreports the steady state.\nA summary. Every value is how many times slower than that row's\nbaseline, so lower is better and 1.0 is the baseline itself:\n\n| workload shape | tree-walker | Truffle | baseline |\n|---|---|---|---|\n| compute-bound loop (primes, Collatz, Fibonacci) | ×59.1 | ×1.8 | Kotlin, 12.9ms/op |\n| Advent-of-Code corpus (14 samples, median) | ×39 | ×28 | Kotlin; on 2 of 14 the tree-walker wins |\n| struct/DTO mapping | ×2.4–4.5 | ×1.0 | Truffle; no Kotlin equivalent written |\n| real library code (FT4, a Rell asset library) | ×1.3–1.6 | ×1.0 | Truffle; serialization, rule evaluation |\n| decimal-heavy numeric code | ×1.1–1.2 | ×1.0 | Truffle; ≥60% of time is JDK BigDecimal |\n\nThe pattern is the classic one. Where the tree-walker's dispatch overhead dominates, partial evaluation removes it: on the compute-bound suite the tree-walker runs ×59.1 slower than hand-written Kotlin, and Truffle ×1.8.\n\n**The ×1.8 that remains is the number worth reading, because of what it\nis measured against. Kotlin here is optimized JVM bytecode, which is the\noutput a run-time bytecode generator would be trying to match, so that\npath sets the ceiling at ×1. Truffle comes within a factor of two of the\nceiling while the source stays an interpreter: interpretation overhead\nwent from ×59 to ×1.8, and no bytecode was emitted.**\n\nWhere the JDK's\n[ BigDecimal](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/math/BigDecimal.html)\n(Java's arbitrary-precision decimal type) dominates, the dispatch was\nnever the cost, and specializing it wins nothing. The fix there was\ndifferent: decimal fast-path leaves that keep values in\n\n`long`\n\nwhen they\nfit, which the report credits on a simplex-noise benchmark. And on two\nsmall samples the tree-walker still wins outright. Benchmarks that only\nbragged would not have told me any of that.Graal's internals are not the interesting part here, and you do not need them: what a backend author works with is partial evaluation as a contract, plus a profiler. The loop was to run the benchmark suites with async-profiler attached, and feed both, the numbers and the profiles of the benchmarks themselves, to the agent, asking for ideas ranked by return. One such prompt, verbatim: \"Analyze profiling data and tell only the most profitable Rell-sided directions to make nodes faster. Judge by return, not by engineering effort.\" The agent proposes; the ranking and the risk policy are mine.\n\nThe policy was tiered. First I greenlit changes that improve the code\nwhether or not they help performance. The main example is removing\nfallbacks from the Truffle backend to the plain interpreter: each one\ndeleted is a performance win and one less coupling between the two\nmodules. It cuts the other way for correctness, and that is worth being\nprecise about. A fallback cannot disagree with the interpreter, because\nit *is* the interpreter; deleting it creates a second implementation\nthat can. Trading a guaranteed-identical slow path for an independent\nfast one is only sane if something checks the two against each other,\nwhich is the differential's whole job. Second came changes that add code\nbut are logical and system-independent, such as keeping a number in a\n`long`\n\n(or a custom 128-bit integer) when it fits instead of allocating\na\n[ BigInteger](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/math/BigInteger.html);\nthe decimal fast-path representations in the benchmark table above are\nthis tier, and they live in the tree as ordinary, readable wrapper\ntypes. Machine-dependent tricks would come last, and mostly did not come\nat all.\n\nThe same policy also kills work, including work already done. The\nGraalVM bytecode DSL (a Truffle facility that generates a bytecode\ninterpreter in place of a tree walker) got the full treatment: built\ninto the backend, benchmarked, profiled, and reverted, because profiling\nnever showed the bytecode path hot, and keeping it meant carrying a\nJava-shaped rewrite for an unproven benefit. The revert was scoped with\ncare: the struct optimization borrowed from SOM (a research Smalltalk\nVM) came from the same chain of refactors, *did* pay off in benchmarks,\nand stayed. Buying the option, measuring it, and killing it is more\nexpensive than not building it, and much cheaper at agent prices than it\nused to be.\n\nThe estimates are a sorting key, not a truth. The benchmark suite is the truth.\n\n**With agents, skip the bridge.** The standard playbook for IR migrations is incremental: keep old and new running side by side, migrate consumers one by one, live with adapters for years. That playbook exists because human bandwidth makes the migration window long. Agents shrink the window to weeks, and at that length the adapters and dual paths cost more than they buy. Decide the end state, cut over in one reviewed move, and spend the saved effort on the harness that proves the cutover.**A serialization requirement is a forcing function.** It rules out the half-measures before they are written, which is stronger than testing them out afterwards: whatever the backend needs has to be expressible as plain data in a language-neutral schema, and a call back into the compiler is not. The round-trip suite is the weaker, second line; it only exercises what the tests reach. If you want a clean boundary, pick a constraint that leaves the unclean options unrepresentable, and let the suite police the residue.**Agents change which options are affordable.** The radical rewrite was always the better design; it was never the rational choice for one person until the mechanical half became cheap. Re-check old design decisions against the new cost of labor: some of them were compromises with a budget, not with reality.\n\n*This is a personal account, not a Chromia publication: opinions,\nframing and any errors are mine. Everything referenced is public:\nrell (the architecture doc and\nthe serialization security analysis are in the repo), and the\nplayground, which\nruns this compiler in your browser.*", "url": "https://wpnews.pro/news/rewriting-a-production-compiler-s-ir-with-ai-agents-in-five-weeks", "canonical_source": "https://github.com/CommanderTvis/writing/tree/main/rr-truffle-rewrite", "published_at": "2026-08-18 11:35:21+00:00", "updated_at": "2026-08-18 11:41:29.708581+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "artificial-intelligence"], "entities": ["Chromia", "Rell", "Truffle", "GraalVM", "JetBrains", "Kotlin", "Roslyn", "LLVM"], "alternates": {"html": "https://wpnews.pro/news/rewriting-a-production-compiler-s-ir-with-ai-agents-in-five-weeks", "markdown": "https://wpnews.pro/news/rewriting-a-production-compiler-s-ir-with-ai-agents-in-five-weeks.md", "text": "https://wpnews.pro/news/rewriting-a-production-compiler-s-ir-with-ai-agents-in-five-weeks.txt", "jsonld": "https://wpnews.pro/news/rewriting-a-production-compiler-s-ir-with-ai-agents-in-five-weeks.jsonld"}}