Rust vs C++ vs Go for Sheaf's Compiler
Feb 10th 2026
Last week I outlined the vision for Sheaf V2. The next question was immediate: what do I write the compiler in?
I come from systems engineering. I know C fairly well and enjoy it: it's clean, small and dependable despite its notorious undefined behaviors. But Sheaf's compiler needs to work with modern ML infrastructure, which means C++.
C++ is the lingua franca of ML systems. MLIR is C++. IREE is C++. XLA is C++. If I want to call MLIR APIs directly, manipulate IR programmatically, or debug the compilation pipeline, C++ is the obvious choice.
Rust, on the other hand, I don't really know. I've read about it and played with the rustlings exercises. I somewhat understand the borrow checker... in theory. But I've never shipped any Rust code. Choosing Rust means learning as I build, or relying on an AI agent to produce code I cannot understand well.
The C++ I Know
I am currently working on CUDA courseworks and diving deeper into IREE, which means a lot of exposure to modern C++.
I like the functional approach that modern C++ offers, reminiscent of Lisp. map, reduce, transform, or tabulate are nice.
But C++ still fights you on the fundamentals. Memory management isn't just about new and delete anymore. It's unique_ptr vs shared_ptr vs weak_ptr, move semantics, and trying to remember if this API takes ownership or just borrows. The type system doesn't help: it compiles, runs, and then segfaults three weeks later because some object got freed and you didn't notice.
Build systems are a nightmare of complexity. CMake is its own language. CMakeFiles can be buggy and let you down 90% down the build process (just submitted a PR for IREE). You vendor LLVM? Great, now your build takes 40 minutes. You link dynamically? Good luck with RPATH on macOS.
I can work in C++, although reluctantly. But maintaining a compiler in C++ over years is something different. Every contributor has to navigate the same landmines. Every refactor triggers a fear that it will break something subtle.
This is not a general argument against C++, but a local optimum for Sheaf’s constraints.
What Rust Actually Offers
I ended up choosing Rust. I know it's trendy, but I chose it for the elegance I felt when I started building the parser.
For instance, algebraic data types match how compilers think. An AST node in Sheaf is:
enum SheafValue {
Integer(i64),
Float(f64),
Symbol(String),
List(Vec<SheafValue>),
Vector(Vec<SheafValue>),
}
When I write match expr, the compiler forces to handle every case. It will not allow to forget Vector and have it crash at runtime. The structure is explicit in the type system.
Of course, C++ has ways to perform the same, using variant or inheritance with dynamic_cast, but it's clunkier.
Rust checks ownership at compile time. This isn't just "no segfaults" (though that's nice), it's that it removes the fear during refactor. If the borrow checker accepts it, the data flow is correct. There's no need for valgrind. No need to trace through pointer lifetimes in my head.
Cargo and its dependencies management just work. No CMakeLists.txt that breaks on a different version of LLVM. No hunting for where libmlir.so got installed.
The Real Cost
Choosing Rust also has real costs, which I am paying for right now.
The main one, obviously: I don't know Rust. I tried asking an LLM to write me some Rust code, and it could have embedded an entire rootkit inside without me noticing. I'm picking up steam, slowly. The main grief is ironically the borrow checker I was praising: it likes to reject things that feel like they should work. I hit lifetime errors I don't fully understand. Claude helps, but I refuse to merge code I don't understand.
MLIR interop could be a problem later. Right now, Sheaf emits text MLIR, so there's no FFI. But if I ever need to manipulate MLIR IR programmatically, I'd need Rust bindings to MLIR's C++ APIs. That's manual work. bindgen would help, but MLIR's template-heavy headers seem like a nightmare to wrap.
The ML systems community speaks C++. Should I need contributors from IREE, XLA, or TensorFlow teams, they know C++, not Rust. Asking them to learn Rust is friction.
But here's the thing: these costs are upfront. I pay them now, during initial development. The entropy cost of C++, the "wait, why did this segfault?" and "why doesn't this build on Linux?" compounds over time. With every platform and every refactor...
The Entropy Principle
One of the goal of Sheaf is to reduce entropy in ML code. That's part the design filters: does this choice add noise or remove it?
Using C++ would add entropy:
- Build complexity (CMake, dependencies, platform quirks)
- Memory management (manual reasoning about ownership)
- Language cruft (40 years of legacy, multiple ways to do everything)
Using Rust removes entropy:
- One build tool (Cargo)
- Ownership checked by compiler
- ADTs that match AST structure
- Exhaustive pattern matching
If I'm building a language about clarity, the compiler should embody that. Not "do as I say, not as I do."
A Word About Go
I actually like Go a lot. It's simple, fast to compile, it's designed by people I highly respect, and the tooling is excellent. go build just works. No lifetime annotations, no borrow checker fights...
For many compilers, Go would be a great choice, but Sheaf's compiler has a specific constraint: exhaustive pattern matching matters more than simplicity.
A compiler is a series of transformations on an AST. At each step, every possible node type must be handled properly. Forget one case, and get a subtle bug that only surfaces when someone writes [1 2 3] instead of '[1 2 3].
In Rust, the compiler enforces this:
match expr {
SheafValue::Integer(n) => ...,
SheafValue::Symbol(s) => ...,
// Forget something -> compilation error
}
In Go, type switches are not exhaustive:
switch v := expr.(type) {
case Integer: ...
case Symbol: ...
// Forget something -> compiles, crashes at runtime
}
For a project that will evolve over years, with contributions from both humans and LLMs, I need the compiler to catch these errors. That's the trade-off: Go's simplicity vs Rust's safety guarantees. For Sheaf, safety won, even though Go would be easier. So, Rust it is.
What Exists So Far
Over the past week, I built a small proof-of-concept to generate MLIR from S-expressions. What I have so far:
- A parser that handles S-expressions, vectors, symbols, literals
- A compiler stub that tracks function definitions and resolves symbols
- A code generator that emits StableHLO text for
(+ 1 2)and(* x y) - Error handling with source locations and meaningful messages
Everything builds and tests pass. The foundation is there. Now comes the real work: function calls, control flow, tensors to make it an actual compiler.
I'll document the implementation as it unfolds.