Goose’s memory model: stable references, worked examples, and the Rust comparison
Explore Goose through three checked code examples, its ownership rules, and a careful reading of the project’s Rust and C++ benchmarks.
Growing a collection while keeping a reference to its first item sounds ordinary. In systems programming, it connects three difficult questions: can storage move, who owns it, and when can it disappear? Goose makes a distinctive trade: constrain how dynamic storage grows and dies so that references can remain stable.
Code to the Moon’s video, “New Language Claims to Be as Safe as Rust, but Faster and Simpler,” prompted this investigation. We checked the video’s description and the original Goose sources. This article explains the design rather than treating the headline as a universal performance result.
Start with where the bytes live
Goose’s dynamic values use compiler-assigned data stacks. Think of a stack here as a reserved region with a marker for its used portion. Appending advances the marker; leaving the owning scope restores it. This is separate from saying every object sits on the native function-call stack. The specification distinguishes native stack storage, data stacks, and static data.
The compiler also tracks which owner bounds a reference’s lifetime. That tracking is intended to reject references that would survive their storage. Goose does not use Rust’s exclusivity and aliasing rules, so the two languages’ safety models should not be described as interchangeable. See the memory and reference rules.
Visual guide: growth and scope exit
Qentrah conceptual illustration, not to scale. Resetting the marker does not imply zeroing bytes or returning pages to the operating system. Based on the data-stack rules. Open full-size diagram.
Example 1: keep a stock reference while the collection grows
This original example stores one featured item, appends a thousand others, and then edits the first item through the reference retained before growth.
struct Stock { sku: i32, quantity: i32 }
fn main() {
var stock: Stock[>..] = [];
let featured .= stock.push(Stock { 501, 4 });
for sku: i32 in 502..1502 {
stock.push(Stock { sku, 1 });
}
featured.quantity += 3;
print("rows=", stock.len, " featured=", stock[0].quantity);
}Our run printed:
rows=1001 featured=7Stock[>..] declares a grow-only array. The .= binding retains a reference to the element returned by push; it is not an independent copy of that stock record. The update through featured therefore changes the first row.
There are 1,000 integers in the half-open range from 502 up to 1502, plus the initial item. The quantity calculation is 4 + 3 = 7. Those two outputs check the intended behavior without pretending to measure speed.
Visual guide: one reference, three moments
Qentrah illustration of Example 1. Box sizes and spacing do not represent measured addresses or record sizes. Open full-size diagram.
The interesting property is that later growth preserves the reference. A design using integer handles can also avoid dangling references, but presents a different interface. The language reference’s array and reference sections explain the available forms:
| Form | Role in an example |
|---|---|
Stock[>..] | Own a collection that can grow |
Stock[4] | Store a fixed number of records |
Stock[:] | View an existing range without owning its elements |
These forms impose different rules; a slice does not extend the owner’s lifetime.
Example 2: make temporary work follow request scopes
A small loop can make ownership visible:
fn main() {
for request in 3 {
var response: u8[>..] = [];
format(response, "request ", request, ": ready");
print(response);
}
}Our run printed:
request 0: ready
request 1: ready
request 2: readyFlowchart: build, use, release, repeat
Qentrah flowchart of Example 2. Cleanup follows the iteration scope; this is not a benchmark or an instrumented memory trace. Open full-size diagram.
Each iteration creates a fresh response builder and uses it before the block ends. The next iteration does not need the previous response. This shape is useful when investigating request scratch buffers or batch transformations whose intermediate values share a lifetime.
The example demonstrates output and scope structure. We did not instrument memory consumption or compare it with a server implementation. The tutorial’s discussion of ownership and scope exit supplies the language-level explanation.
Example 3: a reference the compiler should reject
Stable storage during growth does not make every reference legal. Here an outer variable attempts to retain a view into an inner scratch buffer:
fn main() {
var permanent: u8[>..] = [];
var held = permanent[..];
{
var scratch: u8[>..] = [];
format(scratch, "temporary");
held = scratch[..];
}
print(held);
}We checked this as a deliberately failing program. The compiler rejected the assignment to held, identifying scratch as a root that does not outlive its destination. It did not produce a runnable example.
Flowchart: why the lifetime check fails
Qentrah illustration of the deliberately rejected Example 3, based on its checked diagnostic and the reference-lifetime rules. Open full-size diagram.
The practical repair is to keep the owning buffer in the outer scope, or finish using the view inside the inner scope. Merely changing the view’s variable name cannot repair the lifetime relationship. This is the distinction between “the container did not move” and “the container still exists.”
Read the benchmark comparison by workload and backend
The pinned results file, generated October 16, 2026, reports these ratios for the clang-backed Goose build. A ratio above 1 favors Goose; below 1 favors the comparator.
| Scope | Comparator | Reported speed ratio |
|---|---|---|
| Geometric mean, 16 large workloads | Idiomatic C++ implementations | 3.54× |
| Geometric mean, 16 large workloads | Hand-optimized C++ implementations | 1.26× |
| Geometric mean, 16 large workloads | Best safe Rust implementation per workload | 1.19× |
records, large | Best safe Rust | 2.02× |
sum, large | Best safe Rust | 0.98× |
calc, large | Best safe Rust | 0.89× |
These are project-reported results, not Qentrah benchmarks. The environment is Windows 11 on a Ryzen 9 9950X3D with 94 GB RAM, clang-cl 22.1.3 and rustc 1.98.0. Measurements select the best of three whole-process runs after two warm-ups, including input construction and teardown. The aggregate is a geometric mean, not a promise for a particular application.
The benchmark design asks where compact representations and allocation behavior help, and includes workloads intended to expose weaknesses. For your own experiment, compare equivalent outputs and record build options, input sizes, teardown policy, and backend. Include a flat numeric workload alongside a structure-heavy one so that a single favorable layout does not decide the whole story.
What 1.19× would mean for a single timed task
Use a hypothetical example, not the aggregate as a measured task: suppose a reference implementation takes 119 milliseconds and the alternative has a speed ratio of 1.19.
The alternative’s time would be 119 / 1.19 = 100 milliseconds. The time saved would be 19 milliseconds, or 19 / 119 ≈ 16.0% of the original runtime. A speed ratio of 1.19 is therefore not the same statement as a 19% reduction in execution time. This calculation excludes startup and any work outside the hypothetical timed task.
Try the examples against the same source revision
We built the compiler on Apple silicon macOS from commit f605b5a07dd2aa3a5cb3f4c9b14002bd3c9d3d1a and ran the two positive examples through its bundled TinyCC backend. The negative example was checked for rejection. We did not run the full benchmark suite or establish the absence of compiler safety bugs.
Our build used CMake and an installed C/C++ toolchain:
git clone https://github.com/aardappel/goose.git
cd goose
git checkout f605b5a07dd2aa3a5cb3f4c9b14002bd3c9d3d1a
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j 4Save an example as stock.goose, then run ./build/goose stock.goose from the checkout. This build leaves optional bundled packages at their default configuration; the examples above do not require graphics or physics packages. Check the pinned build instructions for platform prerequisites and the compiler testing guide before drawing broader conclusions.
Decide whether the ownership shape fits your program
Goose is worth understanding when a large share of your work consists of constructing, traversing, and discarding structured data. Before attempting a port, sketch the lifetimes: which values last for one iteration, which for one request, and which must survive independently? That sketch is a useful result even if you keep your current language.
Read the tutorial’s constraints alongside its examples. Whole-program compilation, restrictions on recursive functions retaining growable values, and the distinction between compact variable-sized elements and random indexing affect real designs. Supported interoperation does not automatically provide the ecosystem of an established language.
A useful first experiment is to reproduce the three examples, change the owner’s scope, and explain each accepted or rejected program. Only then choose a representative workload and compare complete programs. That turns the video’s headline into questions you can actually test.
Sources reviewed October 21, 2026. Code and result links are pinned to the reviewed commit; the live project may change. Goose logo: original project repository.

