The Goose Programming Language
A memory-safe systems language with no heap allocator, garbage collector, or lifetime annotations. In this repository's benchmarks, Goose runs faster than C++ and Rust while using less memory.
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.
Why Goose
- Faster than C++ and than safe Rust, while memory safe. Over sixteen benchmarks Goose runs at 3.3x the speed of idiomatic C++, 1.16x hand-optimized C++ and 1.12x the best safe Rust, on 1.9x, 1.3x and 1.2x less memory. These gains come from Goose's data layouts and memory model.
- No allocator, no GC, no reference counting, no destructors. Memory is a handful of data stacks that the compiler assigns statically. Freeing a million-element structure is one store, however deeply it nests.
- Growth preserves references. A reference into a growing array stays valid for as long as the array does. You keep typed references where C++ must
reserveand safe Rust usesu32indices in the corresponding benchmarks. - 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. - Nested data stays inline. A string, an array of strings, a record with variable-size fields and an array of those records are each one contiguous block with no pointer in it. A record that is 160 bytes and an allocation in C++ is 29 bytes and none in Goose.
- Enums that cost what they hold. Variable-mode ADTs give each value its own variant's size rather than the largest one's: 4x less memory and 2x the speed of a Rust
enumon the benchmark that exercises it. - Links narrower than pointers. A relative reference stores a typed, checked link as a 1, 2 or 4-byte offset. Structures built from them are position independent, so your data structure is already its file format: saving is a write, loading is a read plus a verification pass that rejects hostile bytes.
- Everything is built in place, guaranteed. A value is constructed at its final destination through any depth of calls.
items.push(parse(line))writes the parsed record straight into the array, and returning a growable array by value costs nothing. - Returns across multiple calls.
return err from loadreturns from a function any number of frames up, statically checked, with no unwinder, noResulttype and no?on every call. - Threads that share nothing. A worker is compiled as a separate program with its own memory, and flat values cross typed queues as a
memcpy. Data races, locks, atomics and memory orderings do not exist in the language. - Generics and higher-order functions with 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. - Plain C in, plain C out. 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.
Features
One memory model: stacks, and scope exit is the free
A program has the native call stack, static data and N data stacks, where the compiler works out N. 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; the runtime tracks only the bump pointers.
Nothing moves, so references survive growth
push returns a reference to the new element, so a program can link elements while building the array. A vector or Vec may reallocate and cannot provide this guarantee. Stable references account for several benchmark gains.
Safe references, no annotations
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 annotations are needed. There are no aliasing or exclusivity rules.
Nested data stays inline
There is no single array type. There is a family that differs only in what happens to the size, and every member is [metadata][elements...], inline and packed, never a pointer to an element block. Strings are just u8 arrays. A struct may contain variable-size parts, and they sit inline in declaration order, so a record is a run of bytes with nothing indirect in it.
Enums in two sizes
Algebraic data types are the only dynamic polymorphism: no inheritance, no vtables. Every ADT can be stored two ways, chosen at the point of use. Fixed mode (Shape) is a tag plus room for the largest payload, indexable and overwritable. Variable mode (Shape..) gives each value exactly its variant's size; the array becomes sequential and a value never changes variant, and in exchange you may take references into a payload.
Links narrower than pointers
A T& is a machine address: eight bytes, never dangling. A relative reference is the same link stored as a narrow offset: T& is measured from the field itself to a target in the same array, so the structure is position independent. References are transparent, .= binds or retargets one, and Node? is a nullable reference that if, guard and assert narrow.
Your data is already a file format
The tree's links are self-relative, so it means the same thing wherever it sits: saving is writing its bytes and loading is reading them back, with no serializer, no schema and no pointer fixups. from_bytes checks the framing, every tag, every length and every link before the bytes become a value, so a corrupt or hostile file is a false, never a wild reference.
Built in place, always
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.
Returning errors across calls
return E from f returns E as the result of the innermost active call of f, however many frames up, and every function in between keeps its plain signature. It is checked statically, so every call of parse_user must lie inside a load call and nothing is uncaught at runtime.
Generics and function blocks
Everything is monomorphized. Type arguments are inferred from argument types; parameters absent from those types can be supplied explicitly. 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.
Threads that share nothing
There is no shared mutable memory. A thread_fn and everything it calls is compiled as its own program with its own data stacks and its own copies of the globals. Values cross through typed queues, one per type, and must be flat, with no references at any depth.
Plain C in, plain C out
An extern fn binds a Goose signature to a C symbol. The compiler emits one C file for the whole program. The bundled TinyCC backend can compile and run that C inside the Goose compiler's process.
Status
Goose is new. What exists today is a whole-program compiler of about thirty thousand lines of C++, the specification, the standard library, the samples, and a test suite that CI runs on Windows, macOS and Linux with an extra sanitizer job.
License
Goose is licensed under the Apache License, Version 2.0.
Authors
Wouter van Oortmerssen: Language Design, Compiler Design, Coding standards.
Claude Fable: Compiler implementation, Benchmarking, Sample & Doc writing.




