Library / SDK
aardappel/goose avatar
aardappel/goose

Goose: a memory-safe systems language with no heap at all

The Goose Programming Language

1,265 stars51 forksC++Apache-2.0

At a glance

What is it?
Goose is a systems language written in C++ that makes two bets. The first is that you do not need a heap: every dynamic value lives inline on a data stack the compiler assigns statically, and freeing a structure is a single store. The second is that you do not need lifetime annotations: the compiler works out what every reference is rooted in, and rejects the references that could outlive their owners. The benchmarks in the repository claim speed and size wins over both C++ and safe Rust.
Who is it for?
Goose fits a systems programmer who wants memory safety without writing annotations and without giving up the layout control that a garbage collected language takes away, and who is willing to accept a young language and a small standard library. It does not fit a project that needs a large existing ecosystem of libraries, and it does not fit somebody who wants a language that already has a decade of production use behind it.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly C++, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The bet: no heap, no annotations

The opening paragraph of the readme states the position. Goose is a memory-safe systems language with no heap allocator, no garbage collector, and no lifetime annotations, and in this repository's benchmarks it runs faster than C++ and Rust while using less memory.

That is a strong claim, so it is worth being precise about which parts are conventional. Memory safety is not novel; Rust has it, and so do a dozen languages that check at run time instead of compile time. What is novel here is the combination of three things that are usually a menu with trade-offs: you get safety, you get speed, and you get neither a garbage collector nor a set of annotations to write.

The mechanism is stated in the next paragraph. Goose has syntax familiar to C and Rust programmers, but there is no heap. Every dynamic value lives inline on a data stack managed by the compiler. Growth advances a pointer, and memory is freed when its scope ends. The language's type and lifetime rules make this memory model safe to use.

So the shape of the design is a conventional static memory model with the allocator removed. A program still has a native call stack, still has static data, and still has a set of data stacks. What it does not have is a place where values are allocated one at a time and freed individually.

That is what makes the language interesting to look at, because the whole performance and safety story follows from one decision rather than from a collection of them. A garbage collected language pays for allocation with pauses and with a collector. A language with manual memory management pays for it with the code you write to get it right. A language with reference counting pays for it with a field in every structure and a cycle leak. Goose says the cost is paid once, at compile time, by working out where each allocation lives.

The language is implemented in C++, it is Apache 2.0 licensed, and the releases are dated by year and number, with the most recent in October 2026. The documentation is split into a tutorial, a reference, a formal specification, a packages page, and a standard library page, and there is a samples directory of thirty-one complete programs.

The data stack, and the free

The first feature section explains the memory model in the terms the compiler uses, and the example makes it concrete.

A program has the native call stack, static data, and a number of data stacks, where the compiler works out the number. A data stack is a large address-space reservation with a bump pointer, and there is no other memory. At most one resizable value is live per stack, and it is always on top, so growth never moves anything and never checks a capacity. All of this is proved at compile time, and the runtime tracks only the bump pointers.

That last clause is the whole implementation cost, and it is remarkable. The runtime does not know which values are alive and does not check anything; the compiler has already proved the invariants, so the only state to maintain is where the top of each stack is.

The example is a loop that grows an array a hundred thousand times.

goose
for round in 3 {
    var scratch: u8[>..] = [];                        // grow-only: growth is a pointer bump
    for i in 100000 { scratch.push((i % 256) as! u8); }
    print("round ", round, ": ", scratch.len, " bytes");
}                                                     // the free: one store to the stack top

The type of the array is written as a grow-only array with a size hint of nothing, and the comment says what that means: growth is a pointer bump. The cast to a byte type shows the type system doing something Rust would do differently, where a cast is a conversion that can fail, and here the exclamation marks on it mark a cast that is checked.

And then the last line, which is the comment that explains the model better than the prose does: the free is one store to the stack top. At the end of the scope, the stack pointer is reset. Everything allocated in that scope is released at once, in a single instruction, no matter how deeply it nests.

The bullet list at the top of the readme puts the same point in a sentence: freeing a million-element structure is one store, however deeply it nests.

That is a genuinely different shape of memory management from anything with a heap. A tree of a million nodes, in this model, costs one store to release, and the cost of releasing is not proportional to the size of the structure. And the discipline it imposes is not a discipline of balancing allocations, it is a discipline of not letting two things be live at once on the same stack, which the compiler checks.

Nothing moves, so references survive growth

The second feature is the one that separates Goose from a language with a conventional arena, and it is a single guarantee: growth preserves references.

A reference into a growing array stays valid for as long as the array does. The readme compares this to what C++ and Rust have to do, where you must reserve capacity in the first case and where safe Rust uses index types rather than references in the corresponding benchmarks, because a vector may reallocate and therefore cannot promise that a reference into it stays valid.

The example is the demonstration.

goose
struct Item { id: i32, weight: f32 }

var items: Item[>..] = [];
let first .= items.push(Item { 1, 0.5 });             // a reference to element 0
for i: i32 in 2..1000001 { items.push(Item { i, 0.0 }); }
first.weight = 99.5;                                  // still valid, a million pushes later

The push returns a reference to the new element, bound with a syntax that is worth noting, and the loop then pushes a million more elements, and the final line writes through the original reference.

The readme explains what the guarantee buys: a program can link elements together while it is building the array, so a structure whose nodes point at each other can be assembled in one pass, with no fix-up phase afterwards. And it says that stable references account for several of the benchmark gains, which is a striking claim, because a stable reference means the data structure can be a graph rather than a tree, and a graph is what real data tends to look like.

The comparison in the readme is that a vector type in C++ or Rust may reallocate and cannot provide this guarantee, so the guarantee is worth paying attention to when you choose a language for something like an interpreter, a document model, or a scene graph, where the nodes point at each other and the construction is incremental.

The other half of the design follows from the same property. Because growth moves nothing, there is never a moment when a reference is invalidated by a reallocation, so the compiler never has to insert a fix-up, and the standard library does not need a whole category of API to keep the two consistent.

Safety with no annotations, and the root rule

The third feature is the one that decides whether the language is usable: memory safe with zero annotations. No lifetime syntax, no aliasing or exclusivity rules, no unsafe. The compiler infers what every reference is rooted in and rejects references that could outlive their owners.

The mechanism is the root. Every reference and slice has a static root, which is the variable that bounds its target's lifetime. A reference must not outlive the variable that owns its target, and it must not access that target through an incompatible type. The compiler infers the roots and specialises functions for them, which is how the annotations become unnecessary.

The error message is the part worth looking at, because error messages are where a language's character shows.

goose
fn longest(a: u8[:], b: u8[:]) -> u8[:] { if a.len >= b.len { a } else { b } }

var outer: u8[>..] = [];
var w = outer[..];
{
    var inner: u8[>..] = [];
    format(inner, "inner text");
    w = longest(outer, inner);     // error: storing a reference rooted at inner,
}                                  //        which does not outlive the destination (§9.2)

The error names the reference's root and its destination. It says which variable the reference is rooted at, and that this root does not outlive the place the reference is being stored, and it cites the section of the specification that defines the rule.

That is a well designed diagnostic. A reader does not have to work out which of the two inputs to the function is the problem, and they do not have to consult the specification to understand what the rule is, though the citation gives them somewhere to go for the full story.

The type in that example is a slice, written with a bracketed type and a colon, and the readme explains what it is for: a slice lets a function accept a range from any array kind without copying its elements. The example given is a split function that returns slices into its input, and a dictionary keyed by the slice type, which stores views of strings rather than copies.

So the shape of the safety model is: a reference is a pointer plus a root, the root is inferred, and the check is that the root outlives the use. There is no borrowing checker with a set of rules to learn, and no annotations, and the cost of the guarantee is paid by the compiler rather than by the reader of the code.

Everything else, from enums to C interop

The rest of the feature list is a set of smaller decisions, and several of them are the kind that only show up once you have written a few thousand lines in the language.

Enums that cost what they hold is the first. A variable mode algebraic data type gives each value the size of its own variant rather than the size of the largest one, and the readme claims four times less memory and twice the speed of a Rust enum on the benchmark that exercises it. That is the classic tagged union optimisation, and it is only possible when the language lets you know the variant statically, which is a consequence of the value oriented design.

Links narrower than pointers is the next. A relative reference stores a typed, checked link as a one, two, or four byte offset, and structures built from them are position independent. The consequence the readme draws is elegant: your data structure is already its file format, so saving is a write and loading is a read plus a verification pass that rejects hostile bytes. That last clause is the security property, and it comes free from the same mechanism.

Everything is built in place, guaranteed, means a value is constructed at its final destination through any depth of calls, and the example is a push of a parsed record straight into an array. Returning a growable array by value costs nothing.

Returns across multiple calls is a syntax feature with a name: an error return from a function any number of frames up, statically checked, with no unwinder, no result type, and no operator on every call. That last part is a real ergonomic win over the error handling style in most languages, and it is the kind of thing a language author adds because it annoyed them.

Threads that share nothing is the concurrency model, and it is the most radical. A worker is compiled as a separate program with its own memory, and flat values cross typed queues as a memory copy. Data races, locks, atomics, and memory orderings do not exist in the language.

That is the model of a process, not a thread, and it comes from the same place as everything else: if the only memory is a set of stacks the compiler assigns, then a second worker is simply a second set of stacks, and the queue between them is the boundary.

Generics and higher-order functions with no overhead follows the same logic. An untyped parameter is generic, function values are compile-time entities, and a filter with a block compiles to the loop it looks like, building its result straight into its destination.

And then the interoperability story, which is the pragmatic part: plain C in, plain C out. Goose compiles to one C file, so it runs wherever a C compiler does and calls C directly. The bundled TinyCC backend compiles and runs a program in-process, with no build step, which is a remarkable thing for a systems language to offer and an obvious convenience for testing.

Finally the documentation set, and the honest framing of the benchmark results: the benchmarks have the numbers, losses included.

The repository, the benchmarks, and what a reader can check

The repository layout is that of a language implementation, and the pieces tell you what state the project is in.

There is a source directory, a standard library directory, a test directory, a documentation directory, a benchmark directory, a samples directory, a scripts directory, a third party directory, a directory for the editor integration, a directory for the syntax highlighting grammar, a directory for the editor extension, and a build file for CMake with its own directory of build modules.

The samples are the most useful thing for a reader. There are twenty-five named files, and the names are a table of contents for the language: a tour, memory, strings, errors, shapes, functions, a sieve, bignum, sorting, sudoku, a maze, huffman, a linked list, a binary search tree, word frequency, records, a calculator, JSON, a virtual machine, a cellular automaton, a ray tracer, image filters, a mandelbrot with threads, calling C, and serialisation.

That list tells you what the author considers worth showing. A virtual machine, a ray tracer, and a threaded mandelbrot are the three programs that together exercise nearly everything a systems language has to be good at: allocation, recursion, floating point, and parallelism. A JSON parser and a serialiser cover the file and text handling that a real project needs on day one.

The benchmark directory is where the headline claims are supposed to be verifiable, and the readme is careful about how to read it. Over sixteen benchmarks, Goose runs at 3.3 times the speed of idiomatic C++, 1.16 times hand-optimised C++, and 1.12 times the best safe Rust, while using 1.9 times, 1.3 times, and 1.2 times less memory respectively. And the gains are attributed to the data layouts and the memory model rather than to anything clever in the compiler, which is a claim you can check by looking at what the layouts are.

The differences in those numbers are worth reading as a spectrum rather than as a ranking. Against idiomatic C++ the margin is large, which is not surprising when one side has to allocate and the other does not. Against hand-optimised C++ it is 16 percent, which is a much more meaningful number, because a hand-optimised C++ implementation is a fair opponent. Against the best safe Rust it is 12 percent, and the memory comparison narrows to 20 percent. Those are the numbers a sceptic would quote, and the fact that they are in the readme at all is a good sign about the project's temperament.

The docs directory holds the tutorial that walks through all of this by example, the reference that covers the language topic by topic, the specification with the exact rules, and the standard library page. Having a formal specification, with numbered sections that error messages can cite, is a sign of a project taking its own language seriously.

And the project is young. The releases are dated by year and number, with two in the same week in October 2026, which is a fast cadence for a language, and the readme links a chat room and a social account rather than a governance document.

Editorial conclusion

Goose fits a systems programmer who wants memory safety without writing annotations and without giving up the layout control that a garbage collected language takes away, and who is willing to accept a young language and a small standard library. It does not fit a project that needs a large existing ecosystem of libraries, and it does not fit somebody who wants a language that already has a decade of production use behind it. Verify first that the specific features you need exist, because a language at this stage has gaps, that the benchmark claims hold for your workload rather than for the sixteen benchmarks in the repository, and that you can accept compiling to a single C file and calling C directly, since that is the integration story rather than a package manager.

Frequently asked questions

What is the Goose programming language?

It is a memory-safe systems language with no heap allocator, no garbage collector, and no lifetime annotations. Every dynamic value lives inline on a data stack the compiler assigns statically, growth advances a bump pointer, and memory is freed when the scope ends. It is implemented in C++ and licensed under Apache 2.0.

How does Goose free memory if there is no heap?

By resetting the stack pointer when the scope ends. A data stack is a large reservation with a bump pointer, at most one resizable value is live per stack and it is always on top, and the compiler proves the invariants, so the runtime only tracks the bump pointers. Releasing a million element structure is one store, however deeply it nests.

How does Goose guarantee memory safety without lifetime annotations?

Every reference and slice has a static root, which is the variable bounding its target's lifetime. The compiler infers the roots, specialises functions for them, and rejects a reference that could outlive its root or that reaches its target through an incompatible type. The error names the root and the destination and cites the specification section that defines the rule.

What are the benchmark claims for Goose?

Over sixteen benchmarks, it runs at 3.3 times the speed of idiomatic C++, 1.16 times hand-optimised C++, and 1.12 times the best safe Rust, while using 1.9 times, 1.3 times, and 1.2 times less memory respectively. The readme attributes the gains to the data layouts and the memory model, and the benchmark results are published with losses included.

How does Goose interoperate with C?

It compiles to a single C file, so it runs anywhere a C compiler does, and it calls C directly through an external function declaration. There is also a bundled TinyCC backend that compiles and runs a program in process, with no build step.

Official sources

  1. aardappel/goose on GitHub
  2. Issues
  3. License: Apache-2.0
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/aardappel-goose.svg)](https://hysenlabs.com/projects/aardappel-goose)

Was this useful?