Note: This design later evolved into the RegionCell mechanism, which is how the language automatically temporarily freezes regions for read-only operations. This post is up to show the history and what led to the newer design.

Vale's hybrid-generational memory is a new memory model that aims to combine all the best parts of existing memory strategies: easy as garbage collection, deterministic as reference counting, and as fast as borrow checking. 0

Note that hybrid-generational-memory is not implemented yet, it's still just a design.

There are three ingredients to make hybrid-generational memory work:

  • Generational references!
  • Static analysis (sometimes referred to as the "automatic borrow checker") that can eliminate generation checks when it knows an object is alive.
  • Scope tethering, to keep an object from getting freed when a local has a reference into it.

Start with Generational References

Hybrid-generational memory is built upon generational references. Recall:

  • Every heap allocation has a u4812 generation number before the object.
  • Non-owning references contain a raw pointer and a u48 "target generation" number. 3
  • Before dereferencing an object, assert that the target generation number matches the allocation's generation number.
Side Notes
(interesting tangential thoughts)

Vale has three release modes:

  • Resilient mode, which is fast and memory safe; it will halt the program when we try to dereference a freed object.
  • Assist mode, for development, to detect potential problems even earlier.
  • Unsafe mode, which turns off all safety.

Resilient mode uses hybrid-generational memory.


u48 means a 48-bit unsigned integer.


We chose 48 bits, but we could push it as high as 60 bits if we adjusted the below inlining mechanisms. 48 bits is more than enough though.


And a u16 offset to know where the generation is relative to the object, see generational references.

Static Analysis: Eliminate Most Liveness Checks

Use static analysis to reduce the number of liveness checks as much as possible. For example:

  • For each dereference, figure out if an in-scope local indirectly owns it. If so, skip the liveness check. For more on this, see HGM Static Analysis, Part 1.
  • Automatically track this information through intermediate stores/loads from struct members, where possible.
  • Automatically track this information through function calls like an automtic borrow checker, where possible.

This static analysis only works when a nearby local holds the owning reference. The scope tethering explained further below will make it work with non-owning locals too.

Add Scope Tethering

The above static analysis only worked when a nearby local holds the owning reference. Now we'll make it work when a nearby local holds a non-owning reference too.

We'll add a u1 "tethered" bit to every allocation, next to the u48 generation number. A local with a non-owning reference can set this bit to 1 to keep its allocation alive. 4 Inside the scope of the local, we can skip all generation checks.

  • When the object is allocated, the tethered bit will be 0.
  • When a local wants to delay the object's destruction, it will:
    • Do a generation check, to see if the object is still alive. If live, load the pointer to the object, otherwise load null. 5
    • Save the old value of the tethered bit. 6
    • Write a 1 to the tethered bit.
  • When the local goes out of scope, it will:
    • Write the old value back to the tethered bit.
  • When the object is deallocated, if the tethered bit is 1, we'll add it to a queue to check later. 7

Not every non-owning local will tether. Static analysis will make a non-owning tether when it's dereferenced several times. Otherwise, it will just allow the generation checks to happen.


Someone letting go of the object's owning reference will still call its destructor, regardless of the tethered bit. If the tethered bit is 1, the destructor will not free the object. Instead, the last tethering local will free the object.


Loading from null is a memory safe operation: it's guaranteed to correctly seg-fault if we load from it.


The old tethered bit will usually be 0, but if another local is tethering the object, it could be 1 already.


Specifically, every time we allocate, we check the front of the queue to see if something's tether has expired, and if so, reuse that object. If not, move it to the back of the queue and ask generational malloc instead. Similar to a free-list!

That's basically it! There are some more things we could do to speed it up even more, using virtual memory, regions, or more static analysis, but we'll stop the explanation here.

Minor Extra Details

To address some frequently asked questions:

  • When we move something across thread boundaries, we must recurse through 8 and:
    • Assert each tethered bit is zero; assert that there are no locals pointing at the object.
    • Increment each generation number, effectively cutting off access to the rest of this thread.
  • When a generation number hits the maximum, don't use that generation number anymore.
    • genFree could slice up the allocation into smaller ones that don't include the initial 8b.


Similar to how Pony scans all incoming and outgoing objects.

Potential Weaknesses

Some potential weaknesses to explore:

  • Storing the generation number at the top of a <=64b allocation means a liveness check won't incur an extra cache miss since we're about to dereference the object anyway, and the entire object is on one cache line. However, for larger objects, it does incur an extra cache miss. Most objects are small, but programs with an unusually large proportion of medium sized objects not in an array could suffer a small performance hit.
  • Adding the offset to every reference could interfere with optimizations. If so, we'll have to write our own LLVM pass. 9
  • In environments without virtual memory 10, memory fragmentation could be worse, because we can't give pages back to the OS. This is mitigated by regions, where region-calling can guarantee no references pointing into a certain region. 11

Presumably, we would make every generational reference have a pointer to the object, and a target generation number, and a pointer to the current generation. The LLVM pass would eliminate the latter.


Every mainstream OS has virtual memory, but WASM does not.


One day, we could write a compactor for Vale which could also help this, though its probably unnecessary.

Vale's Vision

Vale aims to bring a new way of programming into the world that offers speed, safety, and ease of use.

The world needs something like this! Currently, most programming language work is in:

  • High-overhead languages involving reference counting and tracing garbage collection.
  • Complex languages (Ada/Spark, Coq, Rust, Haskell, etc.) which impose higher complexity burden and mental overhead on the programmer.

These are useful, but there is a vast field of possibilities in between, waiting to be explored!

Our aim is to explore that space, discover what it has to offer, and make speed and safety easier than ever before.

In this quest, we've discovered and implemented a lot of new techniques:

  • Generational Memory, for a language to ensure an object still exists at the time of dereferencing.
  • Higher RAII, a form of linear typing that enables destructors with parameters and returns.
  • Fearless FFI, which allows us to call into C without risk of accidentally corrupting Vale objects.
  • Perfect Replayability, to record all inputs and replay execution, and completely solve heisenbugs and race bugs.

These techniques have also opened up some new emergent possibilities, which we hope to implement:

  • Region Borrow Checking, which adds mutable aliasing support to a Rust-like borrow checker.
  • Hybrid-Generational Memory, which ensures that nobody destroys an object too early, for better optimizations.
  • Seamless concurrency, the ability to launch multiple threads that can access any pre-existing data without data races, without the need for refactoring the code or the data.
  • Object pools and bump-allocators that are memory-safe and decoupled, so no refactoring needed.

We also gain a lot of inspiration from other languages, and are finding new ways to combine their techniques:

  • We can mix an unsafe block with Fearless FFI to make a much safer systems programming language!
  • We can mix Erlang's isolation benefits with functional reactive programming to make much more resilient programs!
  • We can mix region borrow checking with Pony's iso to support shared mutability. a lot more interesting ideas to explore!

The Vale programming language is a novel combination of ideas from the research world and original innovations. Our goal is to publish our techniques, even the ones that couldn't fit in Vale, so that the world as a whole can benefit from our work here, not just those who use Vale.

Our medium-term goals:

  • Finish the Region Borrow Checker, to show the world that shared mutability can work with borrow checking!
  • Prototype Hybrid-Generational Memory in Vale, to see how fast and easy we can make single ownership.
  • Publish the Language Simplicity Manifesto, a collection of principles to keep programming languages' learning curves down.
  • Publish the Memory Safety Grimoire, a collection of "memory safety building blocks" that languages can potentially use to make new memory models, just like Vale combined generational references and scope tethering.

We aim to publish articles biweekly on all of these topics, and create and inspire the next generation of fast, safe, and easy programming languages.

If you want to support our work, please consider sponsoring us on GitHub!

With enough sponsorship, we can:

  • Work on this full-time.
  • Turn the Vale Language Project into a 501(c)(3) non-profit organization.
  • Make Vale into a production-ready language, and push it into the mainstream!