Skip to content
HN On Hacker News ↗

Ante: A New Way to Blend Borrow Checking and Reference Counting

▲ 124 points 29 comments by g0xA52A2A 1w ago HN discussion ↗

Pangram verdict · v3.3

We believe that this document is fully human-written

0 %

AI likelihood · overall

Human
100% human-written 0% AI-generated
SEGMENTS · HUMAN 6 of 6
SEGMENTS · AI 0 of 6
WORD COUNT 1,686
PEAK AI % 1% · §4
Analyzed
Jun 30
backend: pangram/v3.3
Segments scanned
6 windows
avg 281 words each
Distribution
100 / 0%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 1,686 words · 6 segments analyzed

Human AI-generated
§1 Human · 0%

Blending borrowing and reference counting without the crashes!

June 28, 2026

Ante has taken the first step towards something we all thought was impossible: blending reference counting and borrow checking without run-time crashes. 0

This is very promising, because it means that someday, we can more easily use each approach when it makes sense without risking crashes. If there was a language like that, I could prototype a game in a flexible way with reference-counting, and gradually migrate it to faster borrow-checked code. 1

No mainstream language has figured out how to combine them seamlessly, even though many have tried.

Rust tried, but when we try to use Rust's reference-counting Rc type, it often requires RefCell (like Rc<RefCell<Spaceship>>) which can crash at run-time if you hold it wrong. 2 3 I wish rustc could check for proper Rc usage at compile-time! 4 5

Swift too has tried, with its new borrowing system, but it has an expensive run-time check which crashes at run-time if you hold it wrong.

It turns out, blending reference counting and borrow checking is hard, for reasons you'll see below.

So let's talk about Ante!

Ante is a work-in-progress! Some parts of this are implemented, some are still theoretical, and the design is still in flux. This is being actively developed as we speak, so follow Ante's progress on its site and discord!

Ante: A New Way to Blend Borrow Checking and Reference Counting

Ante

Shape-Stability

Reference Counting

More Explicit Syntax

Unions

references

How helps with unions

uniq Conversion

Across Function Calls

Returning Values

I think Ante is onto something here

The broader picture

Conclusion

Appendix: Comparison to Rust's Cell

0

To be more specific, Ante has found a way to blend reference counting and borrow checking for mutable objects, without run-time panics or overhead that come from Rust's RefCell or Swift's exclusivity checking.

§2 Human · 0%

1

This is close to the same reason I work on Vale! But Vale uses generational references, which often risk these exact kinds of program halts.

2

For example, two people getting unique (read-write) references to it at the same time would crash it.

3

It also has try_borrow() and try_borrow_mut() methods which don't panic, but just moves the problem somewhere else.

4

And alas, GhostCell / QCell doesn't count, they bring in other limitations and aren't a drop-in replacement for RefCell.

5

There is Cell, but it doesn't let you get references to its innards. For example you can’t go from &Cell<(T, U)> to a &Cell<U>. Rust also tends to have a lot of friction when internal mutability is used in general, leading to many users avoiding it.

Ante

Ante aims to be a simpler Rust, a systems programming language with memory-safety and thread-safety. It has single ownership and borrow checking, so values are inline (on the stack, or in the containing struct/array).

And when the user wants to prioritize simplicity, they can opt into reference counting with the shared keyword on their types.

For example, this snippet would balance a red-black tree:

// Color can be either an R or a B shared type Color = | R | B

// RbTree can either be an Empty or a Tree shared type RbTree t = | Empty | Tree Color (RbTree t) t (RbTree t)

// A balance function, for RbTree of any element type t balance (tree: RbTree t) {Copy t}:

§3 Human · 1%

RbTree t = match tree | Tree B (Tree R (Tree R a x b) y c) z d | Tree B (Tree R a x (Tree R b y c)) z d | Tree B a x (Tree R (Tree R b y c) z d) | Tree B a x (Tree R b y (Tree R c z d)) -> Tree R (Tree B a x b) y (Tree B c z d) | other -> other

I normally favor C-style syntax, but even I have to admit, this is beautiful. And it's concise too, as small as the Python equivalent, and smaller than the C++ equivalent and Rust equivalent.

But the most interesting thing for me is what Ante is doing in memory safety. Ante has shared mutability superpowers: if you want to mutably borrow reference-counted data, you don't need to risk run-time errors. No mainstream language can do that, not even Rust or Swift.

Before I talk about its shared mutability superpowers, let's start basic. Let's see how it does borrow checking, and then we'll add reference counting into the mix.

Shape-Stability

Ante has a concept of shape-stability, which means "a reference to something of stable shape is always valid 6 no matter what mutations are made elsewhere."

Because of this, Ante code can safely have multiple mutable borrow references to the same struct at the same time.

To set the stage, here's a heal function taking two mutable references to Entitys:

type Entity = energy: I32 health: I32

heal (healer: mut Entity) (target: mut Entity) = healer.energy -= 10 target.health += 10

In Ante, we can call heal with the same Entity for both parameters — for example, when an entity heals itself:

self_heal (entity: mut Entity) = heal entity entity

Mutating healer can't invalidate shared references to the Entity in any way, 7 so the compiler accepts this code as valid.

§4 Human · 1%

In other words, even though healer and target might point to the same Entity, this is memory safe: nothing here can destroy the Entity, so both references stay valid.

Now let's get a little more complicated.

Ante code can actually have multiple mutable borrow references to the same struct, or any of its struct fields, or any of their struct fields, at the same time.

For example, here we have a mutable borrow reference pointing at the ship, and another mutable borrow reference pointing at the ship's engine, at the same time.

type Engine = fuel: I32

type Spaceship = engine: Engine name: String

refuel (ship: mut Spaceship) = engine_alias: mut Engine = ship.engine

// Can still use original `ship` ship.engine.fuel := 200 engine_alias.fuel := 100

Ante knows this is completely memory safe, because during this function, nobody can destroy ship, and therefore nobody can destroy its engine or its fuel.

In other words, these references are to shape-stable data.

Those familiar with Rust and Swift will be surprised by this:

In Rust, we can't have multiple &mut references pointing to the same data. 8

In Swift, we also can't have multiple &mut references pointing to the same data.

Now let's add some reference counting into the mix!

6

Here, "valid" means dereferenceable; you can dereference the reference without risking any memory unsafety or undefined behavior.

7

Similar to how in Rust you can't use a &mut Spaceship to destroy the Spaceship it's pointing at. Even in Rust, it would be perfectly safe to have multiple &mut Spaceship references pointing at the same Spaceship local variable.

8

We can sometimes get close with Cell, but see the appendix section for why Ante goes further than Rust's Cell.

Reference Counting

The above ability is a perfect fit with reference counting, as we'll see below.

In Ante, if you put shared in front of a type definition, it means that type is automatically reference counted.

§5 Human · 0%

9

And when you have a shared mut type, you can mutate its fields without locking, like set_fuel is doing to Spaceship's engine: Engine field:

type Engine = fuel: I32

shared mut type Spaceship = engine: Engine name: String

launch (var ship: Spaceship) = set_fuel (mut ship.engine)

set_fuel (engine: mut Engine) = engine.fuel := 100

I'll explain this snippet further below, but for now: this is similar to the Rust equivalent and Swift equivalent, but without the crash risk of Rust's .borrow_mut() or Swift's automatic run-time exclusivity checking.

launch can make a mut Engine borrow reference because launch knows the engine will stay alive because launch is keeping the containing Spaceship alive.

More generally: you can always make a mut borrow reference to a shared mut type's fields, even though they are shared. 10

9

At least for now. Eventually, the application (not the code) will be able to configure how their memory is managed, e.g. via RC, GC, or a custom mechanism.

10

Though, you can't always take a mut borrow reference to the things inside those fields, as we'll see later. There are different mechanisms for that.

More Explicit Syntax

From here on out, I'll be using the more explicit Rc Spaceship syntax instead of the shared mut type syntactic sugar (you'll see why later).

With the more explicit Rc syntax, the above snippet would look like this: 11

type Engine = fuel: I32

type Spaceship = engine: Engine name: String

launch (var ship: Rc Spaceship) = set_fuel (mut ship.engine)

set_fuel (engine:

§6 Human · 0%

mut Engine) = engine.fuel := 100

Differences:

shared mut type Spaceship became type Spaceship

var ship: Spaceship became var ship: Rc Spaceship

11

We're assuming an Ante syntax where we can access fields of an Rc's contents. Without it, this might instead be something like set_fuel (ship.as_mut ()).engine

Unions

Unions are really good for speed. 12 We like unions.

Unfortunately, unions are notoriously unsafe, and it's hard for memory-safe languages to support them well.

To see why, look at the below example, where Engine is a union, and we're trying to do unsafe shenanigans with it.

type Engine = | StringTheoryEngine (str: String) | ImpulseEngine (fuel: I32)

type Spaceship = engine: Engine name: String

launch (var ship: Rc Spaceship) (var other_ship: Rc Spaceship) = match uniq ship.engine | StringTheoryEngine str -> other_ship.engine := ImpulseEngine 0x42 str.[0] := 'z' | ImpulseEngine fuel -> ()

This program can actually segfault if we pass the same Spaceship in for both ship and other_ship!

For this reason, Ante needs to refuse to compile the above. Ante has this rule:

If you have a mut borrow reference to a union, you cannot make a mut borrow reference to one of its variants.

This is the opposite of the struct rule from before:

If you have a mut borrow reference to a struct, you can make a mut borrow reference to one of its fields.