A Shader Loop That Never Ran: What GPU Readback Found in gogpu/naga, a Go WGSL Compiler
---
title: "A Shader Loop That Never Ran: What GPU Readback Found in gogpu/naga, a Go WGSL Compiler"
published: true
description: "naga compiles WGSL to SPIR-V, Metal, HLSL and DXIL in pure Go, without CGO or DXC. Its DXIL passed Microsoft's validator and still skipped a loop on real GPUs. Why validators can't catch that, and how we test by running the output."
tags: go, testing, gamedev, opensource
cover_image: dev-to-uploads.s3.us-east-2.amazonaws.com
---
Here's a WGSL loop:
for (var j = 0u; j < 4u; j++) {
result[j] = j + 1u;
}
Until v0.20.3 in October, naga, our shader compiler written in Go, turned it into DXIL that never ran the loop. Microsoft's validator looked at the module and said it was valid. On an Intel and an NVIDIA GPU the loop body didn't execute once.
The validator wasn't wrong. That's the interesting part.
---
What the validator saw
This is the DXIL that naga v0.20.2 writes for that loop, trimmed to the lines that matter, with my comments on the right:
%2 = alloca i32, align 4 ; var j
br label %3
...
%5 = load i32, i32* %2, align 4 ; read j for "j < 4"
%6 = icmp ult i32 %5, 4
br i1 %6, label %11, label %12
The variable j gets memory, and the very first thing the loop does is read it. Nothing ever stored the 0. Reading uninitialized memory is legal in LLVM IR, so every instruction here is well formed, the types match, the control flow is structured. IDxcValidator checks exactly those things and reports VALID (S_OK).
What it doesn't check is whether the module computes what the source says. No static validator does, not DXC's and not spirv-val. They answer "can a driver run this?", and the answer was yes. The driver ran it, j started as whatever was in that memory, and the loop exited before its first iteration.
naga v0.20.3 fixed three bugs of this kind in the DXIL backend:
| The shader | What the DXIL did |
|---|---|
| for (var j = 0u; j < 4u; j++) | never stored the initializer; the loop didn't run |
| for (var j = i; j < n; j++) | an optimization pass deleted the initial store, then noticed the loop also writes j; the loop started from an uninitialized j |
| v[i] = v[i] + 1u on a local vector | a run-time index always picked v.x |
For this article I compiled all three shaders with v0.20.2 again. All three come back VALID (S_OK).
Our own tests missed them too. We compare naga's text output with Rust naga's and its DXIL with DXC's, and none of that asks what the shader computes. What found them was a one-off experiment: run the shaders on real GPUs through Direct3D 12 and read the buffers back.
---
What naga is
naga reads WGSL, the WebGPU shading language, and writes SPIR-V for Vulkan, MSL for Metal, GLSL for OpenGL, HLSL for Direct3D, and DXIL for Direct3D 12. The DXIL is emitted directly, without DXC or LLVM. It's plain Go with no CGO, so it cross-compiles like any other Go package.
package main
import (
"fmt"
"log"
"github.com/gogpu/naga/dxil"
"github.com/gogpu/naga/hlsl"
"github.com/gogpu/naga/msl"
"github.com/gogpu/naga/spirv"
"github.com/gogpu/naga/valid"
"github.com/gogpu/naga/wgsl"
)
const source = `
@group(0) @binding(0) var<storage, read_write> result: array<u32>;
@compute @workgroup_size(1)
fn main() {
for (var j = 0u; j < 4u; j++) {
result[j] = j + 1u;
}
}
`
func main() {
module, _, err := wgsl.Parse(source, wgsl.ParseOptions{})
check(err)
_, err = valid.Validate(module, valid.Options{})
check(err)
spv := must(spirv.Compile(module, spirv.Options{})) // Vulkan
metal := must(msl.Compile(module, msl.Options{})) // Metal
hlslSrc := must(hlsl.Compile(module, hlsl.Options{})) // Direct3D, through FXC or DXC
blob := must(dxil.Compile(module, dxil.Options{})) // Direct3D 12, no DXC needed
fmt.Printf("SPIR-V %d bytes, MSL %d bytes, HLSL %d bytes, DXIL %d bytes\n",
len(spv), len(metal), len(hlslSrc), len(blob))
fmt.Println(metal)
}
// must returns a backend's output and stops on its error.
func must[T, I any](out T, _ I, err error) T {
check(err)
return out
}
func check(err error) {
if err != nil {
log.Fatal(err)
}
}
With naga v0.21.2 it prints SPIR-V 1240 bytes, MSL 917 bytes, HLSL 667 bytes, DXIL 2960 bytes, then the Metal source. The zero value of every options struct is the safe choice: bounds checks are on and workgroup memory is zeroed. The SPIR-V, MSL and HLSL writers also give every loop an iteration limit, because a loop the driver's compiler can prove never ends is undefined behavior, and the compiler is then free to delete it, bounds checks included. Two of those show up in the Metal output:
while(true) {
if (metal::all(loop_bound == uint2(0u))) { break; }
...
result[metal::min(unsigned(_e6), (_buffer_sizes.size0 - 0 - 4) / 4)] = _e8 + 1u;
}
Keep an eye on that metal::min. It's a fix from this month, and it comes back later.
naga takes its IR and much of its design from Rust naga, the shader compiler of Rust wgpu, which is the core of WebGPU in Firefox, Servo and Deno. We run Rust naga's 172-shader WGSL test corpus, compare our output with Rust's wherever Rust writes the same format (SPIR-V only structurally so far), and record every difference with its reason. Rust naga has no DXIL backend, so for DXIL the references are DXC and Mesa. naga's main user is gogpu/wgpu, a WebGPU implementation in Go, which compiles its WGSL shaders with naga in its default pure-Go build.
---
Running the output instead
After v0.20.3 we turned that experiment into a permanent test corpus: small compute shaders, each paired with the exact values a GPU has to write when it runs them. It lives in testdata/readback and has 55 cases today, mostly loops, function calls and local variables. Each case is a .wgsl file and a .json file:
{
"format": 1,
"desc": "for-loop counter declared in the loop header writes 1..4; the sentinel after them stays",
"entry": "main",
"workgroups": [1, 1, 1],
"bindings": [
{"binding": 0, "kind": "storage-rw", "len": 5, "fill": "0xDEADBEEF",
"expect": [[1, 2, 3, 4, "fill"]]}
]
}
Three habits made the corpus useful:
Sentinels. Every output buffer starts filled with 0xDEADBEEF, and the expected values say where it has to survive. A loop that runs one time too many, or a store through the wrong index, shows up as a missing sentinel.
Inputs from buffers. Most cases read their inputs from a storage buffer, so the compiler can't fold the work away at compile time, and a few, like collatz-lit.wgsl and collatz-rt.wgsl, test the same computation both ways. The WebGPU conformance test suite uses the same trick.
Known failures with a card. A case can list a backend it currently fails on, with the issue that tracks it. The run stays readable while the bug is open, and the entry goes away with the fix.
naga can't drive a GPU itself, so the work is split. naga checks that every case is well formed and compiles on every backend, and runs spirv-val, glslangValidator, dxc and IDxcValidator on the output where they're installed. wgpu runs the corpus on its GPUs before naga releases that touch a backend, with a Go workspace (go.work) pointing at the naga candidate, so a change runs on real hardware before it's tagged. That runner still lives outside the wgpu repository; moving it in is planned, and then anyone can run the corpus on their own GPU.
For v0.21.1 the corpus ran on eight backend and adapter pairs:
| Backend | Adapters |
|---|---|
| Vulkan | Intel Iris Xe, NVIDIA CMP 30HX |
| Direct3D 12, HLSL through FXC | Intel Iris Xe, NVIDIA GTX 1660 SUPER |
| Direct3D 12, naga's DXIL | Intel Iris Xe, NVIDIA GTX 1660 SUPER |
| OpenGL ES | Intel Iris Xe |
| wgpu's software backend | CPU |
There's no Mac in that list. Metal is covered by wgpu's own tests on a macOS CI runner, which is where the third bug below turned up.
---
What it caught
Local arrays that forgot their initializers. wgpu found this one with a vertex shader that picks its corners from local arrays. The same shape, cut down:
@vertex
fn vs(@builtin(vertex_index) v: u32) -> @builtin(position) vec4f {
var xs = array<f32, 6>(-1.0, 1.0, -1.0, -1.0, 1.0, 1.0);
var ys = array<f32, 6>(-1.0, -1.0, 1.0, 1.0, -1.0, 1.0);
return vec4f(xs[v % 6u], ys[v % 6u], 0.0, 1.0);
}
On naga v0.21.0 the DXIL for this has two alloca instructions and not a single store, so every index reads uninitialized memory, and the validator again says VALID (S_OK). wgpu's shader drew nothing on Intel. The bug was there since at least v0.17.16. Since v0.21.1 naga stores the initializers: 12 stores for this shader, checked on v0.21.2.
A loop-carried variable read as if the loop ran once. naga's DXIL backend turns local variables into SSA values where it can. When every access to a variable sat in one block inside a loop, and that block read the variable before writing it, the pass treated the block as running only once:
var x: u32;
for (var i = 0u; i < n; i++) {
x += i + 1u;
dst[i] = x; // want 1, 3, 6, 10. DXIL wrote 1, 2, 3, 4.
}
Every iteration saw the value from before the loop. This one shipped in every release since v0.17.4. The corpus case for it also checks shapes the old code got right, such as variables read after the loop or updated only in branches, so the fix can't quietly break those.
The v0.21.1 run compared every result with the same run on v0.21.0. 54 of the 55 cases ran on each of the eight pairs (one needs shader-f16, which the runner skipped), 432 results in all. Seven changed, all on DXIL, all from fail to pass:
| Backend and adapter | v0.21.0 | v0.21.1 |
|---|---|---|
| Direct3D 12 + naga's DXIL, Intel Iris Xe | 50 of 54 pass | 54 of 54 |
| Direct3D 12 + naga's DXIL, GTX 1660 SUPER | 51 of 54 pass | 54 of 54 |
| the other six pairs | unchanged | unchanged |
Not everything in that run was green, and both versions failed the same cases: two workgroup-memory cases on the CMP 30HX under Vulkan (not diagnosed yet; Intel passes them), one known OpenGL ES failure, and 15 of the 54 in wgpu's software backend (11 failures, 4 already known). For a release gate that's the point: nothing got worse.
An out-of-bounds write on Metal. wgpu has hand-written readback tests too. One of them, TestStorageBufferOutOfBoundsContained, writes past the end of a storage binding and then checks that the binding next to it is untouched. WGSL allows an implementation to clamp such an access, and wgpu's Metal backend asks naga to clamp every index into a buffer.
On the macOS CI runner the test failed. For a plain var<storage> a: array<u32>, naga's MSL used the index as it was, and the write landed in the next binding. Only a runtime-sized array at the end of a struct got clamped. v0.21.2 clamps every index into a runtime-sized array; that's the metal::min from the output above. The unclamped MSL was perfectly valid MSL.
---
When Rust naga and the spec disagree
While fixing the Metal bug I noticed that our bound was computed from the array's stride and Rust naga's from the element's size. I switched to Rust's form and called ours a defect.
It wasn't. WGSL defines how many elements a runtime-sized array has, and calls it NRuntime: the buffer binding's size minus the array's offset, divided by the stride, rounded down. arrayLength must return exactly that.
The two forms differ when an element is smaller than its stride and the binding ends partway through the last stride. Take array<vec3<f32>>: each element is 12 bytes and the stride is 16. On a 28-byte binding the spec says there is truncate(28 / 16) = 1 element. Rust naga's MSL computes 1 + (28 - 12) / 16 = 2, because a second vec3 would fit in bytes 16 to 27. That's true about memory and wrong about WGSL.
The correction came from the wgpu side, and wgpu's vec3 test now pins it: its comment quotes the spec's NRuntime and requires arrayLength to follow it. v0.21.2 uses the spec's count for both the clamp and arrayLength, which is what naga's HLSL, DXIL, GLSL and SPIR-V output already did. On the macOS CI runner, a 28-byte array<vec3<u32>> binding now reads arrayLength back as 1 (run result). The difference from Rust is written down in the code, in the tests that pin the bound, and in the CHANGELOG.
So this is how we decide now. Where the spec defines a value or an error, the spec decides. Rust naga decides what the spec leaves open, like the shape of the IR or the wording of an error message. When we differ from Rust, we assume the bug is ours until we've read the spec. After that it might be theirs.
---
What I'd do differently
Run the output from the first day of a backend. We had validators, golden files and Rust parity long before we had a GPU in the loop. All three check that the output looks right. Only running it checks that it is right.
Read a green validator as "legal", not "correct". It's a gate worth having, and it says nothing about values.
Make the expected values hard to fake. Sentinels in every slot that shouldn't change, inputs from buffers, and several shapes per bug, including the ones the old code already handled.
---
What's next
DXIL is opt-in in wgpu (GOGPU_DX12_DXIL=1). The default Direct3D 12 path is HLSL through FXC, so DX12 users on that path were never affected by the DXIL bugs above.
Before we call DXIL stable, the shaders of gg, our 2D graphics library, have to pass the validator, readback has to be green on Intel and NVIDIA, and no silent bug may be known. Two are known and open right now:
- more shapes of the local-initializer bug: indexed vectors, arrays of arrays, zero-initialized arrays of vectors, and struct or array variables of a helper function called inside a loop;
- primitive_index reads wrong in fragment shaders on Intel, probably because the DXIL doesn't mark fragment system values as non-interpolated.
If naga compiles one of your shaders wrong, the corpus format is documented. A WGSL file plus the values it should produce is the most useful bug report we can get.
---
naga: gogpu/naga · pkg.go.dev
The Go gopher was designed by Renée French and is licensed under CC BY 4.0.
go get github.com/gogpu/naga@v0.21.2
go install github.com/gogpu/naga/cmd/nagac@latest
nagac -o shader.spv shader.wgsl
Why software is converging on Rust in the age of AI
For about a decade, "rewrite it in Rust" was a joke. It was what an excitable stranger posted under every bug report in a C++ project, and the reply was some polite version of no, because rewrites took years and broke things that already worked. Dropbox spent years moving its sync engine from Python to Rust, and its own write-up warns that in many companies it would have been a terrible idea.
Over a few months in 2026, one large project after another did it anyway, and none of them took years. In February, Andreas Kling moved the JavaScript engine of the Ladybird browser from C++ to Rust in about two weeks, and it passed all 52,898 tests of the official JavaScript conformance suite with byte-for-byte identical output. In May, Bun, the JavaScript runtime, went from 535,000 lines of Zig to Rust in 11 days, with up to 64 Claude agents working through roughly 16,000 compiler errors. Meta merged a Rust version of the React Compiler in June and describes most of it as written by AI. By September, a single engineer at GitHub directing up to 22 agents had turned the TypeScript runtime behind Copilot into 832,000 lines of Rust. And in October, a solo developer named Brandon Thomas published open-source Rust clones of Photoshop, Illustrator and Premiere, built by directing Claude, and collected 70,000 GitHub stars in three weeks.
ForrestKnight's video on this has the best one-line summary I've seen: Rust didn't change this year, the cost of rewriting did. It's 25 minutes and worth it.
{% embed youtube.com %}
He's right, and it's half the answer. Cheap rewrites explain why teams are switching languages now. They don't explain why so many are switching to the same one, when Go, Zig and modern C++ were all available. The other half is about who reads the code. When an agent produces a thousand lines an hour, nobody reviews all of it, so whatever the compiler checks for free ends up carrying most of the weight. Rust's compiler checks more than any other mainstream language's, and agents turn out to be good at taking its feedback.
Rewrites used to take years
Choosing a language used to be one of the few permanent decisions in a project. You could swap a database or a cloud provider, but changing languages meant pausing feature work for a year or more while a team re-implemented everything and chased down the behaviour that shifted along the way. Jarred Sumner, who created Bun, described language choice for a project like his as having been "a one-way decision" until very recently.
The fish shell is a good picture of the old cost. Its move from C++ to Rust, finished in 2024 without AI, took almost two years and more than 200 contributors to port 57,000 lines. Compare that with this year:
| Port | From | Size | Time | Who |
|---|---|---|---|---|
| fish shell (no AI) | C++ | 57K lines | almost 2 years | 200+ contributors |
| Ladybird's LibJS | C++ | ~25K lines of Rust | ~2 weeks | Andreas Kling, with Claude Code and Codex |
| Bun | Zig | 535K lines | 11 days | One engineer, up to 64 agents at once |
| Copilot runtime | TypeScript | ~430K lines in, ~832K lines of Rust out | 14.5 weeks | Mostly one engineer, up to 22 subagents |
| React Compiler | TypeScript | +123K lines | merged June 2026 | Meta, "majority coded by AI" |
You can see the result on GitHub. A project's language bar is computed from its source files, and Bun's now looks like this:

These numbers deserve some care. Bun's port cost around $165,000 in API usage and needed one very experienced engineer watching it the whole time. Sumner's own estimate for doing it by hand was three engineers for about a year. Kling describes Ladybird's port as "human-directed, not autonomous". And Bun is owned by Anthropic, whose models did the work, so its write-up is partly a showcase. Even so, the cost of switching languages has fallen by something like two orders of magnitude.
One thing shows up in every port that worked: a test suite that doesn't care what language the code is in. Bun's tests are TypeScript that drives the runtime from outside, so the same 60,000 tests could judge the Zig version and the Rust version. Ladybird had test262, the official JavaScript conformance suite. Agents translate quickly, and the test suite is what tells you whether the translation is right.
The big players are betting on the same idea. In December 2025, Microsoft Distinguished Engineer Galen Hunt set a goal of "1 engineer, 1 month, 1 million lines of code" for AI-driven translation of C and C++ into Rust, then clarified that Windows isn't being rewritten and the work is a research project. DARPA runs a program called TRACTOR, short for Translating All C to Rust, with the same aim.
Why teams wanted to rewrite: speed and memory
Cheap rewrites only matter if someone wants one, and the usual reason is performance. Rust compiles to native machine code, like C and C++, and it has no garbage collector: memory is freed the moment its owner goes away, not in periodic sweeps. That raises throughput and keeps latency flat instead of spiking whenever a collector runs.
The published before-and-after numbers are large when the old code was garbage-collected or interpreted:
| Project | Compared with | Result |
|---|---|---|
| Cloudflare's core proxy | its NGINX and LuaJIT predecessor | 10 ms faster median response, on under half the CPU and well under half the memory |
| AWS Aurora DSQL | a tuned Kotlin version | about 10x the throughput: roughly 30,000 transactions per second against 2,000 to 3,000 |
| Discord's Read States service | the Go version | the latency spikes Go's garbage collector caused every few minutes went away |
| vLLM's frontend | the Python frontend | median time to first token from 166 ms to 50.5 ms |
| Vite 8's Rolldown bundler | Rollup, in JavaScript | Linear's production build from 46 seconds to 6 |
| ty, Astral's Python type checker | Pyright | an incremental check in 4.7 ms instead of 386 ms |
| GitHub's Copilot runtime | the Node version | no more Node host taking about 100 MB per SDK client |
Ports from C, C++ or Zig look different, because those languages were already fast. Bun got 2 to 5% faster after its move from Zig, and Claude Code, which runs on Bun, started about 10% quicker on Linux (517 ms to 464 ms). The bigger change was memory. Building a 60-module project 2,000 times in one process used 6,745 MB on the Zig version, which leaked about 3 MB per build, and 609 MB on the Rust one.
Part of the speedup from leaving JavaScript or Python comes from going native at all. Microsoft's Go port of the TypeScript compiler made full builds 8 to 12 times faster with no Rust involved. Where Rust pulls ahead of Go and the JVM is the missing garbage collector, which gives steady tail latency and a smaller memory footprint, and that's what Discord and the Aurora DSQL team said they were after.
The compiler became the first reviewer
Cheap rewrites move the bottleneck. GitHub's port produced 832,000 lines of production Rust and another 469,000 lines of tests in three and a half months. No one read that line by line. The checking moves to things that can keep up: the test suite, other agents set up as hostile reviewers, and the compiler.
Of those, the compiler is the one that runs on every single change, in seconds, and never gets tired or changes its mind. Bun's agents used the output of cargo check, Rust's fast type and borrow check, as their to-do list. They burned down those 16,000 errors crate by crate before running a single test. Sumner explained the choice of Rust over a stricter Zig style guide in one line: compiler errors are a better feedback loop than a style guide. A style guide depends on someone noticing the violation. A compiler error stops the build.
Here's what that loop looks like on a small change, with two real compiler errors in it. Press play, or step through it.
> There's an interactive figure here. Try it on the original post.
There's research behind this now. A 2026 paper on generative compilation tested seven models on repository-sized Rust tasks. Left alone, the models wrote Rust that didn't compile 65.9% of the time on average; the paper is frank that Rust is harder to get right on the first try than permissive languages like C. Feeding the compiler's errors back after generation cut that to 20.7%, and checking partial programs while the model was still writing cut it to 13.1%. In one setup, a frontier model went from 51.7% non-compiling to zero.
What surprised me is how well agents do with Rust once that loop is in place. On SWE-bench Multilingual, a benchmark built from real GitHub issues in nine languages, the tested agent resolved Rust issues at the highest rate of any language, 58%. C and C++ came last at 29%. There are only 43 Rust tasks, so I wouldn't lean hard on the exact number, but it points the opposite way from the usual worry that agents would struggle with Rust because there's less of it to learn from. And in GitHub's Copilot port, the dreaded ownership and borrowing errors made up just 1.7% of all the compile errors the agents hit. Most were ordinary slips with names and types.
What Rust's compiler catches that others don't
Every typed language checks something. What matters is how many kinds of bug get turned into compile errors, and Rust covers several kinds that other languages leave to tests, sanitizers or luck. Pick a bug and see when each language finds it:
> There's an interactive figure here. Try it on the original post.
Memory bugs
Use-after-free, double free and reading past the end of a buffer are the classic C and C++ bugs, and they're behind most serious security holes. Microsoft has said for years that about 70% of the CVEs it assigns are memory-safety bugs, and Chromium reports the same share of its high and critical bugs. In safe Rust, the ownership rules make all of these compile errors. Bun's last Zig release notes read like a catalogue of them: use-after-frees in node:zlib and node:http2, a double free in the CSS parser, leaks on error paths.
Android has the longest track record. Google has written new Android code in Rust since 2019, and reported in November 2025 that memory-safety bugs had dropped below 20% of Android's vulnerabilities for the first time, down from 76% in 2019. Android's C and C++ code runs at roughly 1,000 memory-safety vulnerabilities per million lines. Its 5 million lines of Rust have had one near miss, about 0.2 per million. Rust changes also had a 4x lower rollback rate and spent 25% less time in code review, and once agents write the code, review time is the part that doesn't shrink.
Data races
Rust tracks which types can safely move or be shared between threads, through two traits called Send and Sync, and refuses to compile code that would share something unsafely. In TypeScript, Go or C++, a data race compiles and fails now and then in production. Daniel Szoke from Sentry built a whole talk, Why Rust is the Ideal Language for Vibe-Coding, around this: the Rust compiler names the exact type that can't cross threads, which points the agent straight at the fix.
Forgotten cases
A match on an enum has to handle every variant. Add a new status, and every match that doesn't handle it becomes a compile error with its location. Agents are good at the change you asked for and worse at finding the six other places it should ripple to. Rust turns that hunt into a list.
Failure paths
Rust has no exceptions and no null. A function that can fail returns a Result, a value that might be missing is an Option, and the compiler makes the caller handle both before touching what's inside. You can tell from a function's signature whether it can fail, which is most of what a reviewer wants to know.
Error messages a model can act on
rustc errors give the exact location, explain why, and often include a suggested edit. The same output comes as JSON, with each suggestion marked by how safe it is to apply automatically, and cargo fix applies the safe ones. The Rust project built this for people years before coding agents existed. For a model in a loop, it's close to a set of instructions.
One toolchain everywhere
Every Rust project builds with cargo, tests with cargo test, lints with cargo clippy and formats with rustfmt. An agent that knows one Rust project knows how to build and check all of them. In C++, the build system alone can take a new human days to figure out.
Rust had all of these properties in 2018, and for years the effort of satisfying its compiler kept a lot of people away. That effort is exactly the part agents are happy to grind through, and the payoff, code that's been checked before anyone reads it, is exactly what you need when nobody can read it all. Niko Matsakis, a longtime lead of the Rust language team, makes a similar case: Rust's old promise that code which compiles usually works matters more when an agent is the one compiling.
Rust is spreading well past the server
The usual story about Rust is infrastructure, like Cloudflare's proxy and AWS's databases, and that's where it paid off first. But over the last two years Rust has moved into places that have nothing to do with servers. Click around:
> There's an interactive figure here. Try it on the original post.
Browsers
Browsers parse untrusted input from every website on earth, so they were early adopters. Firefox has had Rust inside since its CSS engine, Stylo, shipped in 2017, and its WebGPU support is built on wgpu, a Rust graphics library. Chrome replaced the FreeType font library with a Rust font stack called Skrifa and removed FreeType from Blink entirely in Chrome 145. Chromium's JSON parser is Rust too, because that's what makes it safe to run on untrusted input at high privilege.
The most interesting browser story is Ladybird, a new engine built from scratch by a nonprofit. In 2024 it picked Swift as its future language. In February 2026 it dropped Swift, because the C++ interop "never quite got there", and moved to Rust, starting with the JavaScript engine that agents helped port in two weeks.

Operating systems
Kernels are where a single memory bug becomes a security hole, and Rust is now inside Windows, Linux and Android. Windows has shipped Rust in the kernel since 2024, starting with the code that handles GDI regions. The Linux kernel declared its Rust experiment over in December 2025, which means Rust is now a permanent part of Linux. Android's Binder driver has a Rust rewrite in mainline, and there are Rust GPU drivers for Arm Mali, Apple silicon and, still early, NVIDIA. In Android itself, new Rust code overtook new C++ in 2025.
Mark Russinovich's RustConf 2025 keynote is the best account of how this went inside Microsoft, including a bug that would have been a privilege escalation in C++ and became a clean, reproducible crash in Rust:
{% embed youtube.com %}
Above the kernel, System76 shipped COSMIC, a whole desktop environment written in Rust from the ground up, as the default in Pop!_OS 24.04.

Graphics, 3D and creative tools
This is the part I know best, because I'm building in it. The 3D editor I'm working on is Rust compiled to WebAssembly, drawing with WebGPU from a worker in a browser tab. Two years ago that would have been an odd choice. Now it's fairly ordinary.
The biggest surprise of the year was PhotoCraft. In October, Brandon Thomas published PhotoCraft, an open-source Photoshop alternative written in Rust, along with sibling projects for Illustrator, Premiere, Lightroom and other Adobe apps, built mostly by directing Claude agents. It has layers, adjustment curves, masks and 67 tools, and it re-saves Photoshop files that match the originals in 307 of 309 test cases.
 with a Curves adjustment layer. The whole app is Rust. Screenshot from the PhotoCraft repository, MIT/Apache-2.0.")
It's honest about being early alpha and not yet a tool for professional work, and whether its "clean-room" process holds up legally is an open question. Adobe wasn't involved and hasn't commented. But a few years ago, a credible Photoshop alternative would have needed a funded company and years of work.
Graphite is another creative tool in the same spirit: a node-based vector and raster editor written in Rust, still in alpha, that mixes layers with procedural, generative editing:

Zed, the code editor, reached 1.0 this year on GPUI, its own GPU-rendered UI framework in Rust, with agents built in. Bevy is the most active Rust game engine, and Vello renders 2D vector graphics entirely on the GPU. Figma moved its multiplayer server to Rust back in 2018.

Developer tools
Python's tooling is now largely Rust underneath. Astral's uv, ruff and ty, whose company joined OpenAI in March, are among the most downloaded packages in the ecosystem. pydantic-core, the Rust engine inside Pydantic, was downloaded about 808 million times in a single 30-day stretch this autumn, roughly twice as often as pip itself. JavaScript went the same way: Turbopack is the default bundler in Next.js 16, and Vite 8 bundles with Rolldown, both written in Rust.
The coding agents themselves are moving too. OpenAI's Codex CLI left Node for Rust to get a dependency-free install and native sandboxing, and is now about 97% Rust. GitHub's Copilot runtime followed, xAI's Grok Build shipped in Rust from day one, and Warp, the terminal that has turned into an agent workspace, has been written in Rust and drawn on the GPU from the start.

The AI stack
The code around AI models has been picking up Rust for years. Hugging Face's tokenizers library and the safetensors file format are Rust, and almost every model download pulls in both. In May, vLLM, the most popular open-source inference server, merged an optional Rust frontend because the Python frontend had become the CPU bottleneck. The proposal argued that, with coding agents around, the old objection that contributors can't read Rust is "minimized".
Where Rust isn't winning
Rust has made little headway in three areas that matter a lot, GPU kernels, research code and games, and in a few others another language won on purpose.
GPU kernels
The code that actually runs AI models is still CUDA C++, plus more and more Python. NVIDIA's recent bets are Python-first: cuTile Python, the CuTe DSL in CUTLASS 4, and native CUDA Python. NVIDIA did announce official CUDA Rust in September, with two tracks called cuda-oxide and cutile-rs, but by its own description neither is production-ready, and it calls CUDA C++ and Python the mature toolchains. The community Rust-CUDA project came back to life in 2025 after three dormant years and is still early. If you write kernels for a living, Rust isn't on your list yet.
Research and training
Research lives in Python because researchers iterate in notebooks, and PyTorch and JAX are Python on top of C++ and CUDA. The biggest inference engines are Python or C++ as well: llama.cpp has about 132,000 GitHub stars and vLLM about 95,000, while the leading Rust ML framework, candle, has about 21,000. Hugging Face's TGI, the best-known inference server with a Rust core, is now in maintenance mode, and its own docs point people to vLLM and SGLang. So far Rust takes the always-on pieces around the model (the server frontend, the tokenizer, the file format), Python keeps the places where people experiment, and CUDA keeps the maths.
Games
Unreal is C++, Unity is C#, and Bevy is on version 0.20. LogLog Games wrote a widely shared post, Leaving Rust gamedev after 3 years, after 100,000 lines: games need fast iteration and hot reloading, and the borrow checker kept forcing refactors when they wanted to try ideas. Agents make refactors cheaper, but they don't change how much game development values trying ten things in an afternoon.
Places where another language won
The most useful counterexample is TypeScript itself. When Microsoft rewrote the TypeScript compiler for speed, it picked Go, and TypeScript 7 shipped this year with full builds 8 to 12 times faster. The team's reasons were specific: they wanted a line-by-line port, Go's style is close to the existing code, garbage collection suits a compiler, and the compiler's syntax trees are full of cycles, which Rust's ownership model makes awkward. Zig keeps strong projects like Ghostty and TigerBeetle, whose authors chose it deliberately. Go still runs cloud infrastructure, from Kubernetes to Terraform. And Armin Ronacher, who knows Rust well, argues that Go and TypeScript are better languages for agents, partly because agents struggle with Rust's macros.
Bugs the compiler can't see
Rust's checks have edges, and agents find them too. Bun's port had 19 regressions, all in code that compiled. In one, a debug_assert! wrapped a call that had a side effect, so release builds quietly skipped the call and hot reloading broke. Cloudflare's November 2025 outage was a Rust proxy calling unwrap() on an error it had never seen before, which crashes by design. Check Point found a Rust panic in the Windows kernel that blue-screened the machine. Code at the boundary with C stays unsafe, around 4% of both Bun's and Android's Rust, and the Linux kernel's first Rust CVE came from an unsafe block. Rust also compiles slowly: in the project's 2025 survey, 55% of respondents wait more than 10 seconds for a rebuild, and an agent running hundreds of builds feels that too.
A panic is a better way to fail than memory corruption, and a crash is better than a silent security hole. Reviewers still have to read for logic errors, panics and unsafe; Rust just makes the list shorter.
Where this is heading
Put together, the stack seems to be sorting itself into layers, and no single language gets all of them. Code that runs all day, handles untrusted input, and has to be both correct and fast is moving to Rust: runtimes, browser engines, kernel drivers, toolchains, agent harnesses, inference frontends, creative tools. Agents are speeding that up twice, by making the port affordable and by doing the part of Rust people found tedious. Code where people are still exploring, like research, prototypes, scripts and UI glue, stays in Python and TypeScript, where the feedback loop is a person trying things. GPU kernels stay wherever NVIDIA's tools are best, which today means C++ and Python. And the huge body of existing C and C++ shrinks slowly, as efforts like Microsoft's research project and DARPA's TRACTOR mature.
That's my bet for the next five years, and parts of it could be wrong. If NVIDIA's Rust tracks mature, kernels could follow. If agents get much better at the first draft, the compiler's job as a reviewer matters less. But the direction over the last two years has been consistent, and it sped up in the last one.
Try it on your own agent
If you run coding agents on a codebase, a few changes help in any language and help most in Rust:
1. Put cargo check in the agent's inner loop, and cargo clippy -- -D warnings plus cargo test before it says it's done. The compiler is the cheapest reviewer you have.
2. Turn on the Clippy lints that catch agent shortcuts: unwrap_used and expect_used outside tests, and undocumented_unsafe_blocks so every unsafe needs a // SAFETY: comment.
3. Before porting anything, make sure the test suite runs from outside the code, in another language or as a black box. That's what made Bun's and Ladybird's ports checkable.
4. Use a second agent with fresh context as a hostile reviewer that only sees the diff and is told to assume it's wrong. Bun ran two of them on every change.
5. Read the diff yourself where the compiler can't help: logic, unsafe, unwrap, and anything that touches money or permissions.
That last step needs you to read Rust. If you don't yet, A Rust primer for humans goes from hello world to reviewing an agent's Rust diff, with Python alongside. For what's on the other side of the pull request, there's what an agent harness is and a walk through OpenCode's source. For the other side of the argument, Cliff Biffle's Common Pitfalls of Rewriting Things in Rust is worth an hour, and for where Linux is heading, so is Greg Kroah-Hartman's Untrusted data in Linux.
---
*I'm Dheeraj, a software engineer at Nutanix Enterprise AI working on agent
harnesses and developer tools. I write up the problems that took me too long to
work out. More at dheerajakula.dev.*
Mentions
A Shader Loop That Never Ran: What GPU Readback Found in gogpu/naga, a Go WGSL Compiler --- title: "A Shader Loop That Never Ran: What GPU Readback Found in gogpu/naga, a Go WGSL Compiler" published: true description: "naga compiles WGSL to SPIR-V, Metal, HLSL and DXIL in pure Go, without CGO or DXC. Its DXIL passed Microsoft's validator and still skipped a loop on real GPUs. Why validators can't catch that, and how we test by running the output." tags: go, testing, gamedev, opensource cover_image: dev-to-uploads.s3.us-east-2.amazonaws.com --- Here's a WGSL loop: for (var j = 0u; j < 4u; j++) { result[j] = j + 1u; } Until v0.20.3 in October, naga, our shader compiler written in Go, turned it into DXIL that never ran the loop. Microsoft's validator looked at the module and said it was valid. On an Intel and an NVIDIA GPU the loop body didn't execute once. The validator wasn't wrong. That's the interesting part. --- What the validator saw This is the DXIL that naga v0.20.2 writes for that loop, trimmed to the lines that matter, with my comments on the right: %2 = alloca i32, align 4 ; var j br label %3 ... %5 = load i32, i32* %2, align 4 ; read j for "j < 4" %6 = icmp ult i32 %5, 4 br i1 %6, label %11, label %12 The variable j gets memory, and the very first thing the loop does is read it. Nothing ever stored the 0. Reading uninitialized memory is legal in LLVM IR, so every instruction here is well formed, the types match, the control flow is structured. IDxcValidator checks exactly those things and reports VALID (S_OK). What it doesn't check is whether the module computes what the source says. No static validator does, not DXC's and not spirv-val. They answer "can a driver run this?", and the answer was yes. The driver ran it, j started as whatever was in that memory, and the loop exited before its first iteration. naga v0.20.3 fixed three bugs of this kind in the DXIL backend: | The shader | What the DXIL did | |---|---| | for (var j = 0u; j < 4u; j++) | never stored the initializer; the loop didn't run | | for (var j = i; j < n; j++) | an optimization pass deleted the initial store, then noticed the loop also writes j; the loop started from an uninitialized j | | v[i] = v[i] + 1u on a local vector | a run-time index always picked v.x | For this article I compiled all three shaders with v0.20.2 again. All three come back VALID (S_OK). Our own tests missed them too. We compare naga's text output with Rust naga's and its DXIL with DXC's, and none of that asks what the shader computes. What found them was a one-off experiment: run the shaders on real GPUs through Direct3D 12 and read the buffers back. --- What naga is naga reads WGSL, the WebGPU shading language, and writes SPIR-V for Vulkan, MSL for Metal, GLSL for OpenGL, HLSL for Direct3D, and DXIL for Direct3D 12. The DXIL is emitted directly, without DXC or LLVM. It's plain Go with no CGO, so it cross-compiles like any other Go package. package main import ( "fmt" "log" "github.com/gogpu/naga/dxil" "github.com/gogpu/naga/hlsl" "github.com/gogpu/naga/msl" "github.com/gogpu/naga/spirv" "github.com/gogpu/naga/valid" "github.com/gogpu/naga/wgsl" ) const source = ` @group(0) @binding(0) var<storage, read_write> result: array<u32>; @compute @workgroup_size(1) fn main() { for (var j = 0u; j < 4u; j++) { result[j] = j + 1u; } } ` func main() { module, _, err := wgsl.Parse(source, wgsl.ParseOptions{}) check(err) _, err = valid.Validate(module, valid.Options{}) check(err) spv := must(spirv.Compile(module, spirv.Options{})) // Vulkan metal := must(msl.Compile(module, msl.Options{})) // Metal hlslSrc := must(hlsl.Compile(module, hlsl.Options{})) // Direct3D, through FXC or DXC blob := must(dxil.Compile(module, dxil.Options{})) // Direct3D 12, no DXC needed fmt.Printf("SPIR-V %d bytes, MSL %d bytes, HLSL %d bytes, DXIL %d bytes\n", len(spv), len(metal), len(hlslSrc), len(blob)) fmt.Println(metal) } // must returns a backend's output and stops on its error. func must[T, I any](out T, _ I, err error) T { check(err) return out } func check(err error) { if err != nil { log.Fatal(err) } } With naga v0.21.2 it prints SPIR-V 1240 bytes, MSL 917 bytes, HLSL 667 bytes, DXIL 2960 bytes, then the Metal source. The zero value of every options struct is the safe choice: bounds checks are on and workgroup memory is zeroed. The SPIR-V, MSL and HLSL writers also give every loop an iteration limit, because a loop the driver's compiler can prove never ends is undefined behavior, and the compiler is then free to delete it, bounds checks included. Two of those show up in the Metal output: while(true) { if (metal::all(loop_bound == uint2(0u))) { break; } ... result[metal::min(unsigned(_e6), (_buffer_sizes.size0 - 0 - 4) / 4)] = _e8 + 1u; } Keep an eye on that metal::min. It's a fix from this month, and it comes back later. naga takes its IR and much of its design from Rust naga, the shader compiler of Rust wgpu, which is the core of WebGPU in Firefox, Servo and Deno. We run Rust naga's 172-shader WGSL test corpus, compare our output with Rust's wherever Rust writes the same format (SPIR-V only structurally so far), and record every difference with its reason. Rust naga has no DXIL backend, so for DXIL the references are DXC and Mesa. naga's main user is gogpu/wgpu, a WebGPU implementation in Go, which compiles its WGSL shaders with naga in its default pure-Go build. --- Running the output instead After v0.20.3 we turned that experiment into a permanent test corpus: small compute shaders, each paired with the exact values a GPU has to write when it runs them. It lives in testdata/readback and has 55 cases today, mostly loops, function calls and local variables. Each case is a .wgsl file and a .json file: { "format": 1, "desc": "for-loop counter declared in the loop header writes 1..4; the sentinel after them stays", "entry": "main", "workgroups": [1, 1, 1], "bindings": [ {"binding": 0, "kind": "storage-rw", "len": 5, "fill": "0xDEADBEEF", "expect": [[1, 2, 3, 4, "fill"]]} ] } Three habits made the corpus useful: Sentinels. Every output buffer starts filled with 0xDEADBEEF, and the expected values say where it has to survive. A loop that runs one time too many, or a store through the wrong index, shows up as a missing sentinel. Inputs from buffers. Most cases read their inputs from a storage buffer, so the compiler can't fold the work away at compile time, and a few, like collatz-lit.wgsl and collatz-rt.wgsl, test the same computation both ways. The WebGPU conformance test suite uses the same trick. Known failures with a card. A case can list a backend it currently fails on, with the issue that tracks it. The run stays readable while the bug is open, and the entry goes away with the fix. naga can't drive a GPU itself, so the work is split. naga checks that every case is well formed and compiles on every backend, and runs spirv-val, glslangValidator, dxc and IDxcValidator on the output where they're installed. wgpu runs the corpus on its GPUs before naga releases that touch a backend, with a Go workspace (go.work) pointing at the naga candidate, so a change runs on real hardware before it's tagged. That runner still lives outside the wgpu repository; moving it in is planned, and then anyone can run the corpus on their own GPU. For v0.21.1 the corpus ran on eight backend and adapter pairs: | Backend | Adapters | |---|---| | Vulkan | Intel Iris Xe, NVIDIA CMP 30HX | | Direct3D 12, HLSL through FXC | Intel Iris Xe, NVIDIA GTX 1660 SUPER | | Direct3D 12, naga's DXIL | Intel Iris Xe, NVIDIA GTX 1660 SUPER | | OpenGL ES | Intel Iris Xe | | wgpu's software backend | CPU | There's no Mac in that list. Metal is covered by wgpu's own tests on a macOS CI runner, which is where the third bug below turned up. --- What it caught Local arrays that forgot their initializers. wgpu found this one with a vertex shader that picks its corners from local arrays. The same shape, cut down: @vertex fn vs(@builtin(vertex_index) v: u32) -> @builtin(position) vec4f { var xs = array<f32, 6>(-1.0, 1.0, -1.0, -1.0, 1.0, 1.0); var ys = array<f32, 6>(-1.0, -1.0, 1.0, 1.0, -1.0, 1.0); return vec4f(xs[v % 6u], ys[v % 6u], 0.0, 1.0); } On naga v0.21.0 the DXIL for this has two alloca instructions and not a single store, so every index reads uninitialized memory, and the validator again says VALID (S_OK). wgpu's shader drew nothing on Intel. The bug was there since at least v0.17.16. Since v0.21.1 naga stores the initializers: 12 stores for this shader, checked on v0.21.2. A loop-carried variable read as if the loop ran once. naga's DXIL backend turns local variables into SSA values where it can. When every access to a variable sat in one block inside a loop, and that block read the variable before writing it, the pass treated the block as running only once: var x: u32; for (var i = 0u; i < n; i++) { x += i + 1u; dst[i] = x; // want 1, 3, 6, 10. DXIL wrote 1, 2, 3, 4. } Every iteration saw the value from before the loop. This one shipped in every release since v0.17.4. The corpus case for it also checks shapes the old code got right, such as variables read after the loop or updated only in branches, so the fix can't quietly break those. The v0.21.1 run compared every result with the same run on v0.21.0. 54 of the 55 cases ran on each of the eight pairs (one needs shader-f16, which the runner skipped), 432 results in all. Seven changed, all on DXIL, all from fail to pass: | Backend and adapter | v0.21.0 | v0.21.1 | |---|---|---| | Direct3D 12 + naga's DXIL, Intel Iris Xe | 50 of 54 pass | 54 of 54 | | Direct3D 12 + naga's DXIL, GTX 1660 SUPER | 51 of 54 pass | 54 of 54 | | the other six pairs | unchanged | unchanged | Not everything in that run was green, and both versions failed the same cases: two workgroup-memory cases on the CMP 30HX under Vulkan (not diagnosed yet; Intel passes them), one known OpenGL ES failure, and 15 of the 54 in wgpu's software backend (11 failures, 4 already known). For a release gate that's the point: nothing got worse. An out-of-bounds write on Metal. wgpu has hand-written readback tests too. One of them, TestStorageBufferOutOfBoundsContained, writes past the end of a storage binding and then checks that the binding next to it is untouched. WGSL allows an implementation to clamp such an access, and wgpu's Metal backend asks naga to clamp every index into a buffer. On the macOS CI runner the test failed. For a plain var<storage> a: array<u32>, naga's MSL used the index as it was, and the write landed in the next binding. Only a runtime-sized array at the end of a struct got clamped. v0.21.2 clamps every index into a runtime-sized array; that's the metal::min from the output above. The unclamped MSL was perfectly valid MSL. --- When Rust naga and the spec disagree While fixing the Metal bug I noticed that our bound was computed from the array's stride and Rust naga's from the element's size. I switched to Rust's form and called ours a defect. It wasn't. WGSL defines how many elements a runtime-sized array has, and calls it NRuntime: the buffer binding's size minus the array's offset, divided by the stride, rounded down. arrayLength must return exactly that. The two forms differ when an element is smaller than its stride and the binding ends partway through the last stride. Take array<vec3<f32>>: each element is 12 bytes and the stride is 16. On a 28-byte binding the spec says there is truncate(28 / 16) = 1 element. Rust naga's MSL computes 1 + (28 - 12) / 16 = 2, because a second vec3 would fit in bytes 16 to 27. That's true about memory and wrong about WGSL. The correction came from the wgpu side, and wgpu's vec3 test now pins it: its comment quotes the spec's NRuntime and requires arrayLength to follow it. v0.21.2 uses the spec's count for both the clamp and arrayLength, which is what naga's HLSL, DXIL, GLSL and SPIR-V output already did. On the macOS CI runner, a 28-byte array<vec3<u32>> binding now reads arrayLength back as 1 (run result). The difference from Rust is written down in the code, in the tests that pin the bound, and in the CHANGELOG. So this is how we decide now. Where the spec defines a value or an error, the spec decides. Rust naga decides what the spec leaves open, like the shape of the IR or the wording of an error message. When we differ from Rust, we assume the bug is ours until we've read the spec. After that it might be theirs. --- What I'd do differently Run the output from the first day of a backend. We had validators, golden files and Rust parity long before we had a GPU in the loop. All three check that the output looks right. Only running it checks that it is right. Read a green validator as "legal", not "correct". It's a gate worth having, and it says nothing about values. Make the expected values hard to fake. Sentinels in every slot that shouldn't change, inputs from buffers, and several shapes per bug, including the ones the old code already handled. --- What's next DXIL is opt-in in wgpu (GOGPU_DX12_DXIL=1). The default Direct3D 12 path is HLSL through FXC, so DX12 users on that path were never affected by the DXIL bugs above. Before we call DXIL stable, the shaders of gg, our 2D graphics library, have to pass the validator, readback has to be green on Intel and NVIDIA, and no silent bug may be known. Two are known and open right now: - more shapes of the local-initializer bug: indexed vectors, arrays of arrays, zero-initialized arrays of vectors, and struct or array variables of a helper function called inside a loop; - primitive_index reads wrong in fragment shaders on Intel, probably because the DXIL doesn't mark fragment system values as non-interpolated. If naga compiles one of your shaders wrong, the corpus format is documented. A WGSL file plus the values it should produce is the most useful bug report we can get. --- naga: gogpu/naga · pkg.go.dev The Go gopher was designed by Renée French and is licensed under CC BY 4.0. go get github.com/gogpu/naga@v0.21.2 go install github.com/gogpu/naga/cmd/nagac@latest nagac -o shader.spv shader.wgsl
Formal Web: a modular Web engine written in Rust and using formal methods Reposting; this time with a focus on why it is not a code dump. The project I would like present is a web engine written in Rust. It does not break new grounds in the sense of being the first Web engine in Rust, because we have had a Web engine for a long time, where I am myself involved as a maintainer and Technical Steering Committee member: Servo. However, this is not a code dump either, because: - Myself, and others, have been working on this since March of this year. - This post is a "full fledged post", which is supported by a linked-to longer essay (see below) - I will below explain the relevance for the Rust community - I will compare it with other Web engines written in Rust. ---- Ok so now that this is out of the way, here is the actual post: gterzian/formal-web is a new prototype of a Web engine written in Rust. It is relevant for the Rust community, because it provides us with something new: - a multi-process graphics stack based on modern Rust components, like Vello and Wgpu, paired with a modular design allowing deeper platform integration, for example integrating with Core Animation and using IOSurface on Apple platforms. - A modular approach to integrating a Javascript engine, so that you can "bring your own engine". Currently V8 and the all Rust Boa are supported out of the box, and other engines, Like the also all Rust Nova, could be integrated through the implementation of various traits. - Use of Blitz components, blitz-dom and blitz-paint, as a kind of DOM engine inside the Web engine. - Use of formal methods to model check multi-process Rust code, based on the approach described at Validating Traces of Distributed Programs Against TLA+ Specifications - Modularity of various other layers, such as: networking, media, and the starting of and communication with process-like sub-components. - An embedder API that makes it easy to do various platform specific integrations, such as using a display link on Mac. - An explicit goal of eventually being able to Ship on IOS in the EU as per Apple's guidelines. How is this different from other Web engines in the Rust ecosystem? Difference with Servo: the graphics stack of Servo does not isolate GPU operations in a dedicated process and it is based on an OpenGL stack. Difference with Blitz: while Blitz does integrate well with Vello/Wgpu, it is not designed around process isolation of either GPU operations or the running of Javascript, making it less suitable for the Web use-case where you essentially run untrusted code downloaded over the internet. While it does use Anyrender as modular render target, it does not offer modularity at the lower-level end of the graphics pipeline. Neither engines allow swapping out JS engines, and neither allow for deep platform graphics integration like Core Animation based compositing on Mac (which can make a large difference in terms of battery life). For a walk-through of the entire flow involved in rendering a frame of Web content, see the introduction essay: How I am building a Web engine. The essay also contains a gif showing the benefits of Core Animation on Mac, where only relevant parts of the screen are re-rendered. Please let me know what you think. I can also use help implementing countless Web standards.
Why software is converging on Rust in the age of AI For about a decade, "rewrite it in Rust" was a joke. It was what an excitable stranger posted under every bug report in a C++ project, and the reply was some polite version of no, because rewrites took years and broke things that already worked. Dropbox spent years moving its sync engine from Python to Rust, and its own write-up warns that in many companies it would have been a terrible idea. Over a few months in 2026, one large project after another did it anyway, and none of them took years. In February, Andreas Kling moved the JavaScript engine of the Ladybird browser from C++ to Rust in about two weeks, and it passed all 52,898 tests of the official JavaScript conformance suite with byte-for-byte identical output. In May, Bun, the JavaScript runtime, went from 535,000 lines of Zig to Rust in 11 days, with up to 64 Claude agents working through roughly 16,000 compiler errors. Meta merged a Rust version of the React Compiler in June and describes most of it as written by AI. By September, a single engineer at GitHub directing up to 22 agents had turned the TypeScript runtime behind Copilot into 832,000 lines of Rust. And in October, a solo developer named Brandon Thomas published open-source Rust clones of Photoshop, Illustrator and Premiere, built by directing Claude, and collected 70,000 GitHub stars in three weeks. ForrestKnight's video on this has the best one-line summary I've seen: Rust didn't change this year, the cost of rewriting did. It's 25 minutes and worth it. {% embed youtube.com %} He's right, and it's half the answer. Cheap rewrites explain why teams are switching languages now. They don't explain why so many are switching to the same one, when Go, Zig and modern C++ were all available. The other half is about who reads the code. When an agent produces a thousand lines an hour, nobody reviews all of it, so whatever the compiler checks for free ends up carrying most of the weight. Rust's compiler checks more than any other mainstream language's, and agents turn out to be good at taking its feedback. Rewrites used to take years Choosing a language used to be one of the few permanent decisions in a project. You could swap a database or a cloud provider, but changing languages meant pausing feature work for a year or more while a team re-implemented everything and chased down the behaviour that shifted along the way. Jarred Sumner, who created Bun, described language choice for a project like his as having been "a one-way decision" until very recently. The fish shell is a good picture of the old cost. Its move from C++ to Rust, finished in 2024 without AI, took almost two years and more than 200 contributors to port 57,000 lines. Compare that with this year: | Port | From | Size | Time | Who | |---|---|---|---|---| | fish shell (no AI) | C++ | 57K lines | almost 2 years | 200+ contributors | | Ladybird's LibJS | C++ | ~25K lines of Rust | ~2 weeks | Andreas Kling, with Claude Code and Codex | | Bun | Zig | 535K lines | 11 days | One engineer, up to 64 agents at once | | Copilot runtime | TypeScript | ~430K lines in, ~832K lines of Rust out | 14.5 weeks | Mostly one engineer, up to 22 subagents | | React Compiler | TypeScript | +123K lines | merged June 2026 | Meta, "majority coded by AI" | You can see the result on GitHub. A project's language bar is computed from its source files, and Bun's now looks like this:  These numbers deserve some care. Bun's port cost around $165,000 in API usage and needed one very experienced engineer watching it the whole time. Sumner's own estimate for doing it by hand was three engineers for about a year. Kling describes Ladybird's port as "human-directed, not autonomous". And Bun is owned by Anthropic, whose models did the work, so its write-up is partly a showcase. Even so, the cost of switching languages has fallen by something like two orders of magnitude. One thing shows up in every port that worked: a test suite that doesn't care what language the code is in. Bun's tests are TypeScript that drives the runtime from outside, so the same 60,000 tests could judge the Zig version and the Rust version. Ladybird had test262, the official JavaScript conformance suite. Agents translate quickly, and the test suite is what tells you whether the translation is right. The big players are betting on the same idea. In December 2025, Microsoft Distinguished Engineer Galen Hunt set a goal of "1 engineer, 1 month, 1 million lines of code" for AI-driven translation of C and C++ into Rust, then clarified that Windows isn't being rewritten and the work is a research project. DARPA runs a program called TRACTOR, short for Translating All C to Rust, with the same aim. Why teams wanted to rewrite: speed and memory Cheap rewrites only matter if someone wants one, and the usual reason is performance. Rust compiles to native machine code, like C and C++, and it has no garbage collector: memory is freed the moment its owner goes away, not in periodic sweeps. That raises throughput and keeps latency flat instead of spiking whenever a collector runs. The published before-and-after numbers are large when the old code was garbage-collected or interpreted: | Project | Compared with | Result | |---|---|---| | Cloudflare's core proxy | its NGINX and LuaJIT predecessor | 10 ms faster median response, on under half the CPU and well under half the memory | | AWS Aurora DSQL | a tuned Kotlin version | about 10x the throughput: roughly 30,000 transactions per second against 2,000 to 3,000 | | Discord's Read States service | the Go version | the latency spikes Go's garbage collector caused every few minutes went away | | vLLM's frontend | the Python frontend | median time to first token from 166 ms to 50.5 ms | | Vite 8's Rolldown bundler | Rollup, in JavaScript | Linear's production build from 46 seconds to 6 | | ty, Astral's Python type checker | Pyright | an incremental check in 4.7 ms instead of 386 ms | | GitHub's Copilot runtime | the Node version | no more Node host taking about 100 MB per SDK client | Ports from C, C++ or Zig look different, because those languages were already fast. Bun got 2 to 5% faster after its move from Zig, and Claude Code, which runs on Bun, started about 10% quicker on Linux (517 ms to 464 ms). The bigger change was memory. Building a 60-module project 2,000 times in one process used 6,745 MB on the Zig version, which leaked about 3 MB per build, and 609 MB on the Rust one. Part of the speedup from leaving JavaScript or Python comes from going native at all. Microsoft's Go port of the TypeScript compiler made full builds 8 to 12 times faster with no Rust involved. Where Rust pulls ahead of Go and the JVM is the missing garbage collector, which gives steady tail latency and a smaller memory footprint, and that's what Discord and the Aurora DSQL team said they were after. The compiler became the first reviewer Cheap rewrites move the bottleneck. GitHub's port produced 832,000 lines of production Rust and another 469,000 lines of tests in three and a half months. No one read that line by line. The checking moves to things that can keep up: the test suite, other agents set up as hostile reviewers, and the compiler. Of those, the compiler is the one that runs on every single change, in seconds, and never gets tired or changes its mind. Bun's agents used the output of cargo check, Rust's fast type and borrow check, as their to-do list. They burned down those 16,000 errors crate by crate before running a single test. Sumner explained the choice of Rust over a stricter Zig style guide in one line: compiler errors are a better feedback loop than a style guide. A style guide depends on someone noticing the violation. A compiler error stops the build. Here's what that loop looks like on a small change, with two real compiler errors in it. Press play, or step through it. > There's an interactive figure here. Try it on the original post. There's research behind this now. A 2026 paper on generative compilation tested seven models on repository-sized Rust tasks. Left alone, the models wrote Rust that didn't compile 65.9% of the time on average; the paper is frank that Rust is harder to get right on the first try than permissive languages like C. Feeding the compiler's errors back after generation cut that to 20.7%, and checking partial programs while the model was still writing cut it to 13.1%. In one setup, a frontier model went from 51.7% non-compiling to zero. What surprised me is how well agents do with Rust once that loop is in place. On SWE-bench Multilingual, a benchmark built from real GitHub issues in nine languages, the tested agent resolved Rust issues at the highest rate of any language, 58%. C and C++ came last at 29%. There are only 43 Rust tasks, so I wouldn't lean hard on the exact number, but it points the opposite way from the usual worry that agents would struggle with Rust because there's less of it to learn from. And in GitHub's Copilot port, the dreaded ownership and borrowing errors made up just 1.7% of all the compile errors the agents hit. Most were ordinary slips with names and types. What Rust's compiler catches that others don't Every typed language checks something. What matters is how many kinds of bug get turned into compile errors, and Rust covers several kinds that other languages leave to tests, sanitizers or luck. Pick a bug and see when each language finds it: > There's an interactive figure here. Try it on the original post. Memory bugs Use-after-free, double free and reading past the end of a buffer are the classic C and C++ bugs, and they're behind most serious security holes. Microsoft has said for years that about 70% of the CVEs it assigns are memory-safety bugs, and Chromium reports the same share of its high and critical bugs. In safe Rust, the ownership rules make all of these compile errors. Bun's last Zig release notes read like a catalogue of them: use-after-frees in node:zlib and node:http2, a double free in the CSS parser, leaks on error paths. Android has the longest track record. Google has written new Android code in Rust since 2019, and reported in November 2025 that memory-safety bugs had dropped below 20% of Android's vulnerabilities for the first time, down from 76% in 2019. Android's C and C++ code runs at roughly 1,000 memory-safety vulnerabilities per million lines. Its 5 million lines of Rust have had one near miss, about 0.2 per million. Rust changes also had a 4x lower rollback rate and spent 25% less time in code review, and once agents write the code, review time is the part that doesn't shrink. Data races Rust tracks which types can safely move or be shared between threads, through two traits called Send and Sync, and refuses to compile code that would share something unsafely. In TypeScript, Go or C++, a data race compiles and fails now and then in production. Daniel Szoke from Sentry built a whole talk, Why Rust is the Ideal Language for Vibe-Coding, around this: the Rust compiler names the exact type that can't cross threads, which points the agent straight at the fix. Forgotten cases A match on an enum has to handle every variant. Add a new status, and every match that doesn't handle it becomes a compile error with its location. Agents are good at the change you asked for and worse at finding the six other places it should ripple to. Rust turns that hunt into a list. Failure paths Rust has no exceptions and no null. A function that can fail returns a Result, a value that might be missing is an Option, and the compiler makes the caller handle both before touching what's inside. You can tell from a function's signature whether it can fail, which is most of what a reviewer wants to know. Error messages a model can act on rustc errors give the exact location, explain why, and often include a suggested edit. The same output comes as JSON, with each suggestion marked by how safe it is to apply automatically, and cargo fix applies the safe ones. The Rust project built this for people years before coding agents existed. For a model in a loop, it's close to a set of instructions. One toolchain everywhere Every Rust project builds with cargo, tests with cargo test, lints with cargo clippy and formats with rustfmt. An agent that knows one Rust project knows how to build and check all of them. In C++, the build system alone can take a new human days to figure out. Rust had all of these properties in 2018, and for years the effort of satisfying its compiler kept a lot of people away. That effort is exactly the part agents are happy to grind through, and the payoff, code that's been checked before anyone reads it, is exactly what you need when nobody can read it all. Niko Matsakis, a longtime lead of the Rust language team, makes a similar case: Rust's old promise that code which compiles usually works matters more when an agent is the one compiling. Rust is spreading well past the server The usual story about Rust is infrastructure, like Cloudflare's proxy and AWS's databases, and that's where it paid off first. But over the last two years Rust has moved into places that have nothing to do with servers. Click around: > There's an interactive figure here. Try it on the original post. Browsers Browsers parse untrusted input from every website on earth, so they were early adopters. Firefox has had Rust inside since its CSS engine, Stylo, shipped in 2017, and its WebGPU support is built on wgpu, a Rust graphics library. Chrome replaced the FreeType font library with a Rust font stack called Skrifa and removed FreeType from Blink entirely in Chrome 145. Chromium's JSON parser is Rust too, because that's what makes it safe to run on untrusted input at high privilege. The most interesting browser story is Ladybird, a new engine built from scratch by a nonprofit. In 2024 it picked Swift as its future language. In February 2026 it dropped Swift, because the C++ interop "never quite got there", and moved to Rust, starting with the JavaScript engine that agents helped port in two weeks.  Operating systems Kernels are where a single memory bug becomes a security hole, and Rust is now inside Windows, Linux and Android. Windows has shipped Rust in the kernel since 2024, starting with the code that handles GDI regions. The Linux kernel declared its Rust experiment over in December 2025, which means Rust is now a permanent part of Linux. Android's Binder driver has a Rust rewrite in mainline, and there are Rust GPU drivers for Arm Mali, Apple silicon and, still early, NVIDIA. In Android itself, new Rust code overtook new C++ in 2025. Mark Russinovich's RustConf 2025 keynote is the best account of how this went inside Microsoft, including a bug that would have been a privilege escalation in C++ and became a clean, reproducible crash in Rust: {% embed youtube.com %} Above the kernel, System76 shipped COSMIC, a whole desktop environment written in Rust from the ground up, as the default in Pop!_OS 24.04.  Graphics, 3D and creative tools This is the part I know best, because I'm building in it. The 3D editor I'm working on is Rust compiled to WebAssembly, drawing with WebGPU from a worker in a browser tab. Two years ago that would have been an odd choice. Now it's fairly ordinary. The biggest surprise of the year was PhotoCraft. In October, Brandon Thomas published PhotoCraft, an open-source Photoshop alternative written in Rust, along with sibling projects for Illustrator, Premiere, Lightroom and other Adobe apps, built mostly by directing Claude agents. It has layers, adjustment curves, masks and 67 tools, and it re-saves Photoshop files that match the originals in 307 of 309 test cases.  with a Curves adjustment layer. The whole app is Rust. Screenshot from the PhotoCraft repository, MIT/Apache-2.0.") It's honest about being early alpha and not yet a tool for professional work, and whether its "clean-room" process holds up legally is an open question. Adobe wasn't involved and hasn't commented. But a few years ago, a credible Photoshop alternative would have needed a funded company and years of work. Graphite is another creative tool in the same spirit: a node-based vector and raster editor written in Rust, still in alpha, that mixes layers with procedural, generative editing:  Zed, the code editor, reached 1.0 this year on GPUI, its own GPU-rendered UI framework in Rust, with agents built in. Bevy is the most active Rust game engine, and Vello renders 2D vector graphics entirely on the GPU. Figma moved its multiplayer server to Rust back in 2018.  Developer tools Python's tooling is now largely Rust underneath. Astral's uv, ruff and ty, whose company joined OpenAI in March, are among the most downloaded packages in the ecosystem. pydantic-core, the Rust engine inside Pydantic, was downloaded about 808 million times in a single 30-day stretch this autumn, roughly twice as often as pip itself. JavaScript went the same way: Turbopack is the default bundler in Next.js 16, and Vite 8 bundles with Rolldown, both written in Rust. The coding agents themselves are moving too. OpenAI's Codex CLI left Node for Rust to get a dependency-free install and native sandboxing, and is now about 97% Rust. GitHub's Copilot runtime followed, xAI's Grok Build shipped in Rust from day one, and Warp, the terminal that has turned into an agent workspace, has been written in Rust and drawn on the GPU from the start.  The AI stack The code around AI models has been picking up Rust for years. Hugging Face's tokenizers library and the safetensors file format are Rust, and almost every model download pulls in both. In May, vLLM, the most popular open-source inference server, merged an optional Rust frontend because the Python frontend had become the CPU bottleneck. The proposal argued that, with coding agents around, the old objection that contributors can't read Rust is "minimized". Where Rust isn't winning Rust has made little headway in three areas that matter a lot, GPU kernels, research code and games, and in a few others another language won on purpose. GPU kernels The code that actually runs AI models is still CUDA C++, plus more and more Python. NVIDIA's recent bets are Python-first: cuTile Python, the CuTe DSL in CUTLASS 4, and native CUDA Python. NVIDIA did announce official CUDA Rust in September, with two tracks called cuda-oxide and cutile-rs, but by its own description neither is production-ready, and it calls CUDA C++ and Python the mature toolchains. The community Rust-CUDA project came back to life in 2025 after three dormant years and is still early. If you write kernels for a living, Rust isn't on your list yet. Research and training Research lives in Python because researchers iterate in notebooks, and PyTorch and JAX are Python on top of C++ and CUDA. The biggest inference engines are Python or C++ as well: llama.cpp has about 132,000 GitHub stars and vLLM about 95,000, while the leading Rust ML framework, candle, has about 21,000. Hugging Face's TGI, the best-known inference server with a Rust core, is now in maintenance mode, and its own docs point people to vLLM and SGLang. So far Rust takes the always-on pieces around the model (the server frontend, the tokenizer, the file format), Python keeps the places where people experiment, and CUDA keeps the maths. Games Unreal is C++, Unity is C#, and Bevy is on version 0.20. LogLog Games wrote a widely shared post, Leaving Rust gamedev after 3 years, after 100,000 lines: games need fast iteration and hot reloading, and the borrow checker kept forcing refactors when they wanted to try ideas. Agents make refactors cheaper, but they don't change how much game development values trying ten things in an afternoon. Places where another language won The most useful counterexample is TypeScript itself. When Microsoft rewrote the TypeScript compiler for speed, it picked Go, and TypeScript 7 shipped this year with full builds 8 to 12 times faster. The team's reasons were specific: they wanted a line-by-line port, Go's style is close to the existing code, garbage collection suits a compiler, and the compiler's syntax trees are full of cycles, which Rust's ownership model makes awkward. Zig keeps strong projects like Ghostty and TigerBeetle, whose authors chose it deliberately. Go still runs cloud infrastructure, from Kubernetes to Terraform. And Armin Ronacher, who knows Rust well, argues that Go and TypeScript are better languages for agents, partly because agents struggle with Rust's macros. Bugs the compiler can't see Rust's checks have edges, and agents find them too. Bun's port had 19 regressions, all in code that compiled. In one, a debug_assert! wrapped a call that had a side effect, so release builds quietly skipped the call and hot reloading broke. Cloudflare's November 2025 outage was a Rust proxy calling unwrap() on an error it had never seen before, which crashes by design. Check Point found a Rust panic in the Windows kernel that blue-screened the machine. Code at the boundary with C stays unsafe, around 4% of both Bun's and Android's Rust, and the Linux kernel's first Rust CVE came from an unsafe block. Rust also compiles slowly: in the project's 2025 survey, 55% of respondents wait more than 10 seconds for a rebuild, and an agent running hundreds of builds feels that too. A panic is a better way to fail than memory corruption, and a crash is better than a silent security hole. Reviewers still have to read for logic errors, panics and unsafe; Rust just makes the list shorter. Where this is heading Put together, the stack seems to be sorting itself into layers, and no single language gets all of them. Code that runs all day, handles untrusted input, and has to be both correct and fast is moving to Rust: runtimes, browser engines, kernel drivers, toolchains, agent harnesses, inference frontends, creative tools. Agents are speeding that up twice, by making the port affordable and by doing the part of Rust people found tedious. Code where people are still exploring, like research, prototypes, scripts and UI glue, stays in Python and TypeScript, where the feedback loop is a person trying things. GPU kernels stay wherever NVIDIA's tools are best, which today means C++ and Python. And the huge body of existing C and C++ shrinks slowly, as efforts like Microsoft's research project and DARPA's TRACTOR mature. That's my bet for the next five years, and parts of it could be wrong. If NVIDIA's Rust tracks mature, kernels could follow. If agents get much better at the first draft, the compiler's job as a reviewer matters less. But the direction over the last two years has been consistent, and it sped up in the last one. Try it on your own agent If you run coding agents on a codebase, a few changes help in any language and help most in Rust: 1. Put cargo check in the agent's inner loop, and cargo clippy -- -D warnings plus cargo test before it says it's done. The compiler is the cheapest reviewer you have. 2. Turn on the Clippy lints that catch agent shortcuts: unwrap_used and expect_used outside tests, and undocumented_unsafe_blocks so every unsafe needs a // SAFETY: comment. 3. Before porting anything, make sure the test suite runs from outside the code, in another language or as a black box. That's what made Bun's and Ladybird's ports checkable. 4. Use a second agent with fresh context as a hostile reviewer that only sees the diff and is told to assume it's wrong. Bun ran two of them on every change. 5. Read the diff yourself where the compiler can't help: logic, unsafe, unwrap, and anything that touches money or permissions. That last step needs you to read Rust. If you don't yet, A Rust primer for humans goes from hello world to reviewing an agent's Rust diff, with Python alongside. For what's on the other side of the pull request, there's what an agent harness is and a walk through OpenCode's source. For the other side of the argument, Cliff Biffle's Common Pitfalls of Rewriting Things in Rust is worth an hour, and for where Linux is heading, so is Greg Kroah-Hartman's Untrusted data in Linux. --- *I'm Dheeraj, a software engineer at Nutanix Enterprise AI working on agent harnesses and developer tools. I write up the problems that took me too long to work out. More at dheerajakula.dev.*
wgpu-powered photo & illustration app for comic and manga artists I've been messing around with libadwaita and wgpu to make an open-source version of one of my favorite drawing software, which unfortunately was not available on linux. Has been a wild journey. I started only a few days after astra and opus 5.5 came out, and made far more progress than I ever dreamed of. I'm the sort of person who cares more about the end result (as the person who actually plans to use this app), than the process to get there (sorry anyone who is skeptical of AI coding). I also recognise that this was all only possible due to the amazing hard work of the rust community who had the insight to create a cross-platform GPU library that literally works everywhere (wgpu-rs). Porting the app to android and IOS was basically fully automatic thanks to rust. Still work in progress (as all sofware will ever be). Specifically looking for people wo want to help vibe-coded some cool wgsl shaders for the brush and effects engine. Check out the code and app here: capyatelier/capycanvas capycanvas.art