Goose Drops the Heap for Compiler-Managed Data Stacks

A new systems language called Goose claims memory safety without the overhead that typically accompanies it. The project, hosted at aardappel/goose on GitHub, removes the heap allocator, garbage collector, reference counting, and destructors entirely. Instead, every dynamic value lives inline on a data stack that the compiler assigns statically. Benchmarks show Goose outperforming both C++ and Rust: across sixteen tests it runs 3.3x faster than idiomatic C++, 1.16x faster than hand-optimized C++, and 1.12x faster than the best safe Rust, while using 1.9x, 1.3x, and 1.2x less memory respectively.

How Data Stacks Replace the Heap

A Goose program has the native call stack, static data, and N data stacks where the compiler determines N. Each data stack is a large address-space reservation with a bump pointer. At most one resizable value is live per stack and it always sits on top, so growth never relocates existing data and never checks a capacity. The compiler proves all of this at compile time; the runtime tracks only bump pointers.

Freeing a deeply nested structure of a million elements takes one store to the stack top. No allocator, no GC, no reference counting, no destructors. Memory is a handful of data stacks that the compiler assigns statically.

Inline Layouts Eliminate Pointer Chasing

Nested data stays inline. A string, an array of strings, a record with variable-size fields, and an array of those records each form one contiguous block with no pointers inside. A record that occupies 160 bytes and requires an allocation in C++ becomes 29 bytes with zero allocations in Goose. Variable-mode algebraic data types store each value at its exact variant size rather than reserving space for the largest variant, yielding 4x less memory and 2x the speed of a Rust enum on the benchmark that exercises it.

Strings are just u8 arrays. A grow-only array u8[>..] acts as a builder, u8[] stores a finished string inline inside whatever holds it, u8[..16] fits a small string inside a struct, and u8[:] provides a view. Structs may contain variable-size parts that sit inline in declaration order, so a record is a run of bytes with nothing indirect in it.

References That Survive Array Growth

Goose guarantees that a reference into a growing array remains valid for as long as the array does. Push returns a reference to the new element, so a program can link elements while building the array. A vector in C++ or a Vec in Rust may reallocate and cannot provide this guarantee; stable references account for several benchmark gains.

Every reference and slice has a static root — the variable that bounds its target's lifetime. A reference must not outlive the variable that owns its target or access that target through an incompatible type. The compiler infers roots and specializes functions for them, so no lifetime annotations are needed. There are no aliasing or exclusivity rules. When a lifetime check fails, the error names the reference's root and its destination.

Narrow Links Make Data Position Independent

A relative reference stores a typed, checked link as a 1, 2, or 4-byte offset. T& is measured from the field itself to a target in the same array, making the structure position independent. T& measures from a named pool's base, allowing other arrays to link into the pool. T&? uses offset 0 as null. Structures built from these links are already their file format: saving is a write, loading is a read plus a verification pass that rejects hostile bytes.

A binary search tree using 12-byte nodes demonstrates this. Each node holds a key and two self-relative links. The tree's links are self-relative, so it means the same thing wherever it sits in memory. The from_bytes function checks the framing, every tag, every length, and every link before the bytes become a value; a corrupt file yields false, never a wild reference.

Copy-Free Construction and Multi-Frame Returns

The copy-free construction guarantee says a constructed value is always built in its final home, propagated top-down through calls. A function returning a grow-only array by value writes its elements straight into the caller's variable or into a field of the record being built inside another array, so out-parameters mostly do not appear. Formatted strings write directly into new elements; filtering, folding, and sorting build results straight into their destination with no temporary.

The return E from f construct returns E as the result of the innermost active call of f, however many frames up. Every function in between keeps its plain signature. It is checked statically, so every call of a parser must lie inside the caller and nothing is uncaught at runtime. It uses a hidden discriminant checked per frame rather than an unwinder, and the error message is built directly where the caller wants it.

Concurrency Without Shared Mutable Memory

A thread_fn is compiled as a separate program with its own data stacks and its own copies of globals. Values cross through typed queues, one per type, and must be flat with no references at any depth. A flat Goose value is contiguous, so crossing is a memcpy. Data races, locks, atomics, and memory orderings do not exist in the language because there is no shared mutable memory.

Generics and higher-order functions carry no overhead. An untyped parameter is generic. Function values are compile-time entities, so xs.filter() { it > 0 } compiles to the loop it looks like and builds its result straight into its destination. Everything is monomorphized; function values are passed as generic parameters, every call is direct and inlinable, they cannot escape, and there are no closure objects or function pointers.

Direct C Interop With a Single C File Output

Goose compiles to one C file, so it runs wherever a C compiler does and calls C directly through extern fn. The bundled TinyCC backend compiles and runs a program in-process with no build step. Only types with supported C representations may cross the boundary; unsupported signatures are rejected. The math and OS modules use this interface. An extern fn binds a Goose signature to a C symbol, letting the language leverage existing C libraries while keeping its own memory model for everything else.

What Developers Gain

For teams building embedded systems, game engines, or high-throughput servers, Goose offers memory safety without runtime overhead. The elimination of heap fragmentation and GC pauses simplifies reasoning about latency. Inline data layouts and stable references reduce cache misses. Position-independent structures mean serialization is a memory copy with built-in verification. The thread model removes an entire class of concurrency bugs by design. C interop means existing libraries are usable without rewriting them, while new code benefits from the compact memory model. The compile-time lifetime inference catches errors before execution with no boilerplate, and the single C file output integrates into existing toolchains.