Skip to content

Intent now compiles itself

Intent now compiles itself

Lance Haig
Jul 16, 2026 · 5 min read
1

Intent now compiles itself

ack in February I wrote about Intent. That post was more about what it couldn't do.

Contracts were only runtime-checked. No generics on user-defined types. I said it was closer to "Rust with mandatory asserts" than to formal verification. I meant it. It was a proof of concept and I said so.

Both of those limitations are gone now. The compiler proves contracts statically with Z3. And v0.4.0 finished generics across package boundaries.

But neither of those is what I want to tell you about.

Intent compiles itself.

What that actually means

"Self-hosting" sounds mystical. It isn't. A self-hosting compiler is one written in the language it compiles. That's the whole folklore version.

I wanted a stricter claim than that. Getting a compiler to run only proves it doesn't crash.

So here's the real version.

Intent has four toolchain artefacts: a formatter, a linter, a checker, and the compiler. Each one exists twice. There's the original Go implementation -- call it stage1. And there's a reimplementation written in Intent.

Stage1 compiles the Intent version into a native binary. That's stage2.

Stage2 compiles the same Intent source again. That's stage3.

Now the test that makes it real. Stage3 has to reproduce the entire toolchain byte-for-byte identical to what stage1 produced.

Not "passes the same tests". Not "behaves equivalently". Identical bytes.

If stage2 had introduced even a subtle divergence while compiling itself, the output drifts and the bytes stop matching.

That's a fixpoint. One command runs the whole chain and diffs the results:

[1/4] building stage2 (stage1 compiles compile_main.intent)...
[2/4] stage2 emits compile_main.intent (the stage3 source)...
[3/4] building stage3 (cargo compiles stage2's emit)...
[4/4] verifying stage3 emits the toolchain byte-equal with stage1...
  selfhost/compiler/compile_main.intent      EQUAL
  selfhost/checker/check_main.intent         EQUAL
  selfhost/formatter/main.intent             EQUAL
  selfhost/linter/lint_main.intent           EQUAL

Summary: 4/4 -- stage3 reproduces the whole toolchain byte-for-byte. Bootstrap is a fixpoint.

I ran that this morning against the current tip. Four for four.

Why I bothered

I'll admit there's a certain satisfaction to it. That wasn't why I did it though.

Intent exists for one reason: correct, auditable code. Every function carries requires/ensures contracts. Every entity carries invariants. The compiler proves them.

So if the language is good enough to hold that standard, what's the most demanding program you could point it at? The compiler.

A byte-equal bootstrap is the most honest test of the language I can think of. The compiler is a real, large, fiddly program. The language had to be expressive enough to write it, and precise enough to reproduce Go's exact output. Every gap in the language showed up as a gap in the bootstrap, which is exactly where you want your bugs to surface.

I built it gap-driven, one piece at a time. Each phase started by adding whatever the language was missing -- string indexing and a Char type, command-line arguments, and so on -- then writing the Intent code that needed it. The formatter came first, because it's a fixpoint on its own source. Then the linter. Then the checker. Then the compiler's IR lowering and Rust backend.

What's new in v0.4.0

Self-hosting itself shipped in v0.3.0. What v0.4.0 adds is the last mile of a harder case: generics across package boundaries.

There's a small bit of new language surface. You can construct generics and enum variants through a module qualifier:

pkg.Pair<Int>()       // a generic constructor from another package
pkg.Variant()         // an enum variant from another package
pkg.Enum.Variant()    // fully qualified

Only the bare forms parsed before, which is a small feature on its own.

The interesting work was carrying it all the way through the self-hosted compiler, so stage2 emits Rust byte-identical to stage1. That meant teaching the Intent backend to monomorphise generic free functions, so a call like identity<Int>() becomes a mangled, specialised identity__Int(...). It meant mangling unqualified calls to imported functions with the defining module's prefix rather than the caller's. It meant emitting a generic's struct even when it's only ever instantiated in the consuming module.

It's the kind of work where "it works" and "byte-identical to the reference" are separated by a long tail of edge cases, and the byte-equal gate is unforgiving about every single one of them.

It also turned something up. There's a latent ordering hazard in the original Go compiler. Monomorphisations emit in map order, which isn't deterministic once you have two or more generics in a module. No program in the corpus hits it, and it has been sitting there quietly the whole time. Writing the second implementation is how you find the bugs the first one was getting away with.

How this actually got built

I didn't write the code. Any of it.

Claude wrote all of it. The Go compiler, the Intent reimplementation, the formatter, the linter, the checker, the IR lowering, the Rust backend. My part was the conversation around it: what the language should be, what a contract ought to say, which way to go when there were two reasonable options, and reading what came back closely enough to know when it wasn't right.

Intent exists because I wanted AI to write code that humans could audit rather than read line by line. I have been doing exactly that for months now, on the most demanding program I could have picked, and the contracts are what made it survivable. I wasn't tracing implementations. I was reading declarations and deciding whether they said what I meant.

The language got good enough to build the thing that builds it, and it got there through the workflow it was designed for.

That carries on from here. There's plenty still to do -- the standard library is thin, the diagnostics could be far better, and I want to find out how far the static proving can be pushed before it stops being practical. Same arrangement: Claude writes it, I guide it and audit what comes back.

Try it

git clone https://github.com/lhaig/intent
cd intent
make build
make bootstrap-stage3    # watch it reproduce itself, 4/4

If you want to see the language rather than the plumbing, INTENT.md is the guide and examples/ has runnable programs.

I'm still more interested in the question than in my particular answer to it. If you try it, I'd like to hear where it feels wrong, what you'd want from it, and whether auditing contracts really does beat reading implementations once someone other than me is holding it.

GitHub: https://github.com/lhaig/intent

Did you enjoy this article?

Recommend it — Standard Reader surfaces well-loved writing to more readers across the network.

Across the AtmosphereDiscussions