Goose Language Bets Systems Code Doesn't Need a Heap
Goose is a memory-safe systems language with no heap, no GC, and no lifetime annotations. What its stack-only design reveals about Rust and regions.

Here's a claim that will annoy at least two language communities: the borrow checker was a detour. Not a mistake — Rust's ownership model solved real problems and dragged the whole industry forward — but a detour past an older, simpler idea that we're now rediscovering. That idea is regions: memory allocated in big stack-like arenas, freed wholesale when a scope ends. The Goose programming language, a new systems language making the rounds this week, is the most aggressive modern bet on that idea I've seen, and its design forces an uncomfortable question: how much of what we put up with in C++ and Rust is actually load-bearing, and how much is just the heap talking?
Goose has no heap allocator. No garbage collector, no reference counting, no destructors, no lifetime annotations, no unsafe. Every dynamic value lives inline on one of a handful of data stacks that the compiler assigns statically. Growth bumps a pointer. Freeing a million-element nested structure is a single store to the stack top. The author reports benchmarks running faster than idiomatic C++ (3.3x) and safe Rust (1.12x) on less memory — and John Carmack's public reaction called out exactly the part I find most interesting, the relative references that make a data structure its own file format. Whether or not Goose ever ships production software, it's a serious thought experiment in what a systems language looks like when you delete the heap and refuse to apologize for it.
The memory model is the whole language
Most language features are negotiable. Syntax, module systems, even error handling — you can imagine two reasonable languages making opposite choices. Memory models aren't like that. Pick one and everything downstream is forced: your data structures, your concurrency story, your FFI, your error messages. Goose understands this and leans all the way in.
The rule is simple to state: a program has the native call stack, static data, and N data stacks, where the compiler works out N. At most one resizable value is live per stack, and it's always on top. That single invariant buys an astonishing amount. Because the growable value never moves — there's nothing above it that would need to be relocated — references into a growing array stay valid forever. In C++, a std::vector<T> that reallocates invalidates every pointer and iterator into it, which is why generations of C++ programmers have been bitten by holding a reference across a push_back. Safe Rust sidesteps this by making it unexpressible; you'd use u32 indices instead, and pay for it in bounds checks and ergonomics. Goose just... lets you keep the reference. The type and lifetime rules are checked at compile time, with the compiler inferring what each reference is "rooted" in and rejecting ones that could outlive their owners — but with no annotations, no aliasing rules, no exclusivity.
struct Item { id: i32, weight: f32 }
var items: Item[>..] = [];
let first .= items.push(Item { 1, 0.5 });
for i: i32 in 2..1000001 {
items.push(Item { i, 0.0 });
}
first.weight = 99.5; // still valid, a million pushes later
If you've spent time with Rust's borrow checker, this reads like a magic trick. Where did the lifetimes go? The honest answer is: into a restriction. Rust's ownership system can prove safety for arbitrary sharing patterns — graphs, cycles, shared mutable structures across scopes. Goose proves safety for a much narrower set of patterns and simply refuses the rest. That trade — expressiveness for simplicity — is the oldest one in language design, and it's the thread I'll keep pulling on here. If you've read our piece on what soundness really means in Rust's borrow checker, Goose is the flip side of that coin: soundness achieved not by a more powerful analysis, but by a smaller language.
Everything inline: data layout as a first-class feature
The second big idea is that there's no single array type. Goose has a family of array kinds that differ only in what happens to the size: fixed at compile time, frozen after construction, growable within an inline capacity, grow-only, grow-shrink. All of them are [metadata][elements...] — contiguous, packed, never a pointer to an element block. Strings are just u8 arrays, so a struct can contain variable-size parts sitting inline in declaration order. A record with a variable-length string field and a variable-length list of sub-records is one run of bytes with zero indirection.
The claimed numbers are eye-catching: an order record that's 29 bytes and zero allocations in Goose, versus 160 bytes and one allocation as C++ std::string + std::vector<Item>, or 153 bytes and four allocations in Rust. Now, one fair correction surfaced in the discussion almost immediately: Goose's own tutorial example of "a vector of strings" ignores small-string optimization, which every mainstream STL implementation does. Short strings in C++ don't allocate. That doesn't kill the argument — SSO helps strings under ~15–22 bytes and does nothing for the general case of nested variable-size data — but it's a reminder that benchmark tables on a language's homepage are marketing, even when the language is good. The losses are in there too, to the author's credit, which is more than most projects publish.
There's a real cost, and Goose doesn't hide it: an array of variable-size elements is sequential. You can iterate it, but you can't index it — element k is wherever the bytes land. If you want random access, you keep a second array of references as an index. This is the kind of trade that data-oriented design people have made by hand for decades (it's basically how columnar formats and flatbuffers-style layouts work), but here it's the default, and opting out means extra work. Whether that's a feature or a straitjacket depends entirely on your access patterns.
Relative references: your data structure is its file format
My favorite idea in Goose is the relative reference, and it's the one Carmack zeroed in on too. A normal reference is a machine address: eight bytes, absolute, meaningless outside the process. A relative reference stores the same link as a 1, 2, or 4-byte offset — measured either from the field itself or from a named pool's base. Structures built from self-relative links are position independent. They mean the same thing wherever they sit in memory. Which means saving a data structure is a write(), and loading it is a read() plus a verification pass.
That last clause matters more than it sounds. The naive version of this trick — memory-map a file full of pointers — is a classic security disaster. Goose's version validates: from_bytes checks the framing, every tag, every length, every link before the bytes become a value, so a corrupt or hostile file gives you false, not a wild pointer. You get the speed of mmap'd structures with something like the safety of a parser.
Carmack's framing was exactly right: "'Parsing' file and network data is often 100x slower than it needs to be." Anyone who's profiled a deserialization hot path knows this. The industry answer so far has been schema-driven zero-copy formats — Cap'n Proto, FlatBuffers — which work but bolt a schema compiler and a runtime library onto your project. Goose's answer is that the in-memory layout and the wire layout are the same thing by construction, because the language never had pointers to absolute addresses in the first place. The binary search tree example is almost cheeky: 12-byte nodes, links included, insert is one push and one store, and the whole tree persists by writing its bytes out. Lispers will recognize the philosophical move — collapse a distinction everyone assumed was fundamental — which is part of why Lisp still matters as a design reference point sixty years on.
We've been here before: regions, arenas, and ML Kit
The historically literate response to Goose arrived on cue in the comments: "I seem to remember the ML guys went down this route. I believe they called it regions." Correct. Region-based memory management goes back to the ML Kit work in the 1990s — Tofte and Talpin's region inference let an ML compiler replace garbage collection with statically-determined stack-like regions, allocating and freeing whole regions without any per-object bookkeeping. And the honest historical verdict is that it was declared something of a dead end: region inference worked, but real programs showed enough pathological memory growth — regions holding dead data alive far too long — that the research community mostly moved on. Garbage collectors kept their crown.
But the idea never actually died; it just changed jobs. Arena allocation is standard practice in game engines, compilers, and high-performance servers. Apache's memory pools, per-request arenas in web servers, the "reset the scratch buffer every frame" pattern in game loops — these are regions by another name, managed manually because the language won't help you. What Goose does differently from the ML Kit is worth stating precisely: instead of inferring regions for a general-purpose functional language (hard, unpredictable), it exposes the stack model directly and makes the programmer's intent part of the type. T[>..] says "this grows, references stay valid." The compiler's job is checking a declared discipline, not discovering one. That's a much easier problem, and it's the same reason Rust succeeded where earlier automatic-lifetime schemes struggled: programmers tolerate a contract they can see.
There's a broader lesson here about how production languages absorb research. Region inference as a fully automatic replacement for GC: failed. Regions as a manual systems pattern: ubiquitous. Borrow checking: a massive success that itself borrows heavily from region ideas and linear types. Erlang-style share-nothing processes: vindicated, and Goose's threads are literally that — workers compiled as separate programs with their own memory, values crossing typed queues as a memcpy, with data races unrepresentable. Nobody gets the whole picture right the first time. Ideas circulate for decades before landing in a form that works.
Where the model breaks down
I said I'd open with a contrarian claim and then concede where it breaks. Here's the concession. The borrow checker is not a detour if your program's sharing patterns are genuinely arbitrary — and an enormous class of programs have exactly that shape. The sharpest question in the Goose discussion was also the simplest: how do you build a hash table mapping keys to strings, where the program inserts and deletes random strings at random times? Deletion is the killer. A grow-only stack can't reclaim a hole in the middle. The suggested workarounds — one stack per size class, periodic compaction, a pool with a freelist where "freeing" a slot just means it may be reused — all work, and the pool design is genuinely clever (a stale slot reads a different value of the correct type rather than corrupting memory). But each one is the programmer re-implementing a piece of a memory manager, by hand, for that data structure.
Similarly: memory use. With no GC, a stack that grows holds everything it ever grew to hold until scope exit. Long-running programs with irregular lifetimes — the JSON-document question raised in the thread, caches, session state — either fit the scope-disciplined pattern or they don't, and when they don't, no amount of elegance saves you. The ML Kit's failure mode (dead data pinned in live regions) doesn't disappear in Goose; it becomes visible and manual, which is arguably better, but it's still there. A commenter framed the right posture: Goose is a language for a restricted purpose, and that's fine. Data-oriented workloads, compilers, game entity systems, anything pipeline-shaped — read, transform, emit, reset — fits beautifully. A long-lived mutable object graph does not.
And one more caveat on the headline numbers. Self-hosted benchmark suites are a genre with known failure modes: you're comparing your best Goose against someone's "idiomatic" C++, and idioms are where fights start. The SSO slip shows the comparison code wasn't adversarially tuned. The 1.12x-over-safe-Rust figure is more interesting than the 3.3x one, precisely because it's small — it says the memory model wins on layout and allocation elimination, not on magic, and a tuned Rust version using arenas and indices would close much of the gap. Which is itself the point: you can write arena-based Rust, but the language makes it awkward, and awkward is what determines what code actually gets written.

What production languages will actually steal from Goose
Almost nobody will write production Goose in the next few years — one-person languages compiling through a single C backend don't get there quickly, and the author says as much about its status. That's not the interesting question. The interesting question is which ideas escape into the languages we already use, because that's how this always works. My predictions, in order of confidence:
- Position-independent structures with verified loading. Zero-copy serialization with a safety pass is too good to stay niche. Expect more language-integrated versions of what FlatBuffers does awkwardly, especially as data-heavy services keep hitting parse costs.
- Arrays as a family, not a type. "What happens to the size?" is a better organizing question than "stack or heap?" Fixed, frozen, capacity-bounded, grow-only — these distinctions exist in every codebase as informal conventions. Making them types is cheap and catches real bugs.
- Compile-time function values. Goose treats function values as compile-time entities, so
xs.filter() { it > 0 }compiles to the loop it looks like. C++ templates and Rust monomorphization already do this; the difference is Goose makes it the only way, removing the runtime-function-pointer footgun entirely. - Statically-checked multi-frame returns.
return err from loadreturns from a function N frames up, checked at compile time, no unwinder, noResultpyramid, no?on every call. Effect systems are circling this same idea from the other direction. One of them will land in a mainstream language this decade.
The share-nothing threading model deserves a mention too, mostly because it's the least novel and the most vindicated: it's Erlang's processes, it's Dart's isolates, it's the actor model without the ideology. That it keeps getting reinvented tells you the industry consensus on shared mutable state is settled in theory and ignored in practice. Goose at least makes ignoring it impossible.
The real argument: simplicity through restriction
Step back and Goose's design is one move, repeated: shrink the set of representable programs until safety becomes cheap to prove. No heap means no dangling heap pointers. Grow-only stacks mean references can't be invalidated. Separate-worker threads mean races can't exist. Copy-free construction means no accidental temporaries. Each restriction deletes a whole category of bug and a whole category of language machinery — the borrow checker's aliasing rules, the destructor, the unwinder — that existed to manage it.
This is Rob Pike's "less is exponentially more" argument with the gloves off, and it lands on the same tension Pike's Go did: restrictions feel like liberation right up until your program is the one the restriction excludes, and then they feel like a wall. Go excluded generics for a decade and survived; it also excluded a lot of correct programs. Goose excludes far more. The reason to pay attention anyway is that Pike's old rules about simplicity have aged well precisely because complexity budgets are real — every guarantee a language offers automatically is a guarantee every programmer using it doesn't have to hold in their head.
So: was the borrow checker a detour? For general-purpose systems programming, no — Rust's expressiveness is genuinely needed for the arbitrary-sharing cases, and its success bought the industry a vocabulary for talking about ownership at all. But for the large, quiet majority of systems code that's pipeline-shaped, arena-shaped, request-shaped? Goose is a proof that we might have been able to have memory safety, speed, and small binaries with a fraction of the ceremony — if we'd been willing to say "no" to more programs. Language design is the art of choosing which programs to forbid. Goose forbids different ones than we're used to, and the result is the most interesting systems language sketch I've read in years. I don't expect to ship it. I do expect to keep stealing from it.
Goose's bet is that a language can forbid more programs than Rust does and still be useful — the benchmarks suggest it's worth taking seriously, losses included.


