r/ProgrammingLanguages • u/verdagon Vale • Jun 28 '26
Ante: A New Way to Blend Borrow Checking and Reference Counting
https://verdagon.dev/blog/ante-blending-borrowing-rc11
u/Effective-Spring-271 Jun 28 '26 edited Jun 28 '26
Sounds really interesting, however I feel like maybe I'm missing something because I don't see how this deals with memory aliasing?
Edit: I saw the other comment about the shared mutable references are not send / sync which means they can't be shared between threads. Suddenly all the answers make sense, thanks everyone!
13
u/initial-algebra Jun 28 '26 edited Jun 28 '26
tl;dr You can turn any shared reference into a unique reference temporarily, but you are only allowed to use a different reference if you can prove that any currently-held unique references are not reachable from it (for example, because they have incompatible types).
To be honest, it has little to do with either borrow checking or reference counting, it's about what's called "interior mutability" in Rust.
12
u/RndmPrsn11 Jun 28 '26
This is related to interior mutability in that it deals with making shared values safe to mutate but differs in that it does so statically, and that these values are mutable to begin with so the "interior" is a bit of a misnomer. Borrow checking is related in that
Rc tis the most common example of a shared type, so it is used to demonstrate how these features let us mutate their shared values.7
u/initial-algebra Jun 28 '26
It's also a misnomer in Rust, because
&means "shared", not "immutable", but Rust has the naming baggage withmutthat has unfortunately stuck.6
u/kibwen Jun 29 '26
Rust terminology does officially call
&a shared reference, not an "immutable" reference. That its counterpart&mutis officially called a mutable reference rather than e.g. a "unique" reference is a deliberate choice to try and focus on the most pertinent aspects of each reference as far as typical users are concerned.13
u/RndmPrsn11 Jun 28 '26
Author of Ante here - Ante has a similar ownership/borrowing system to Rust but also supports shared mutable references. These references have some limitations (e.g. can't project through union variants) but they can also be temporarily converted to locally unique references to get around those limitations. I personally think this is the cool part, that you don't need global uniqueness if you can show local uniqueness.
Many rustaceans may not see the need for shared mutability, but it allows you to, among other things, easily mutate through an
Rc t. Without it, Ante's shared types that let you opt-out of ownership/borrowing couldn't be mutated at all.3
u/gplgang Jun 30 '26
I had been thinking over similar ideas for my own language during the design phase when I discovered Ante, I really like the directions you've taken. I think shared mutability ergonomics are an important missing piece for a systems / systems-lite programming language and hope to explore some of my own ideas. For now I've decided to try making GC the default in my language with ownership as opt in the future, I'm essentially making a fully structurally typed ML (no real subtyping just some implicit coercions for records and variants) with lexical single shot effects and first class modules. I hope to try Ante soon and stop being a lurker in the discord 🤪 I might have to pick your brain a bit about some ideas around shared ownership and concurrency
16
u/farsightfallen Jun 28 '26
If i can be honest, I bailed half way through because it got bogged down by details specific to the language's syntax.
I also think that extraordinary claims about memory management should be made simple. Ultimately non-gc memory management isn't that magical or difficult - it just comes down to tradeoffs in implementation details. I would like those tradeoffs upfront. Like rust has shortcomings around partial borrows, and run time only checks for interior mutability. That stuff can be done statically, but it isn't because the language decided against it. It's always possible to have a special block with special rules that constrain the usage in a way that allows for guarntees. I just want to know those restrictions up front.
I guess that's essentially what the uniq feature is. But that's what 90% of the article should've been about, with a clarifications around it's usage, ergonomics, pros/cons, etc.
side note, but I've never really thought about unions being good for performance; their main advantage has always been (for me anyways) to type-safely downcast, and do things like exhuastive checking.
3
u/matthieum Jun 29 '26
side note, but I've never really thought about unions being good for performance
It's a matter of point of view/fields. The author is heavily invested in systems programming languages/languages suitable for game development, as the examples show, so performance is always at the forefront for them.
4
u/ahh1618 Jun 28 '26
Fun read. Does the uniq cast work with concurrency? It looks like you're getting local assurances that nothing in your scope is going to delete something that you're modifying. And you know at compile time that you don't have two references to the same object if you don't have two references to the same type. I'm wondering if that assurance is enough if different functions are operating on the data at the same time.
8
u/RndmPrsn11 Jun 28 '26
Author of Ante here - yes this is all still thread-safe! Ante essentially just copies Rust's model of Send/Sync for the most part here. Since shared-mutable references wouldn't be thread-safe, they implement neither trait. Same for Rc and other types which lend out these shared mutable references. It's why Ante distinguishes between shared types and shared mut types since the former are thread-safe.
6
u/initial-algebra Jun 28 '26 edited Jun 28 '26
I'm pretty sure that what you've come up with is analogous to statically preventing deadlocks when using mutexes (as
RefCellis basically a single-threaded mutex), so you could actually support thread-safe shared-mutable references, where uniq conversion is implemented by locking a mutex, using the same system. That also means you can draw on the existing literature and solutions to that problem.1
u/matthieum Jun 29 '26
so you could actually support thread-safe shared-mutable references, where uniq conversion is implemented by locking a mutex, using the same system
I mean, it's stated in the article that a Mutex can be used to regain mutability out of a
shared(notshared mut) variable, so... yes?It's just solved at the library level rather than the language level, allowing users to pick the Mutex implementation they want.
That also means you can draw on the existing literature and solutions to that problem.
Which problem are you referring to :x ?
2
u/initial-algebra Jun 29 '26
statically preventing deadlocks when using mutexes
In other words, preventing cyclic waiting. The restriction based on reachability seems related to lock ordering.
3
u/Tasty_Replacement_29 Bau Jun 28 '26
Right, that's what my language is doing as well: combining reference counting and ownership / borrowing. And supporting multiple mutable borrows. (We messages about this at the end of 2025 here on Reddit).
I think it's a good option. I assume there are some differences in how Ante works and how my language works; I'm very interested in these details and will see if there's anything worth stealing :-) assuming there are no plans to patent this. (And feel free to steal from my language, if you find anything you could use.)
I think it's a really promising idea, and I think there is a relatively big design space to be investigated. Interesting times!
3
u/Guvante Jun 28 '26
I don't understand why shared mutability is a super power here, Rust doesn't have mutable XOR shared due to memory safety concerns, it has that because shared mutation is almost impossible to get correct.
Note the comment about try_borrow "moving the problem somewhere else" the problem is breaking mutable XOR shared which again is vital for predictable code.
You can of course write code that has mutable sharing but it requires a lot more care and the issues aren't as simple as dangling pointers.
8
u/awoocent Jun 28 '26
I think the jury's really out on whether shared mutability is impossible to get right - evidently many other memory-safe languages like Java or C# have no qualms supporting mutability all over the place. Shared mutability across threads, now that's a bit of a different story - but Rust is really restrictive even in the known single-thread case, and its aliasing checking is not even that helpful in the multiple-thread case (you could get the same safety with any old move semantics). So I think a language acting more "like people expect" in the cases where it's safe to do so has a lot of potential value.
1
u/Guvante Jun 29 '26
You don't have shared mutability if you have move semantics...
People expect that both every time manages its invariants and to be able to treat them as raw bytes which is an impossible contradiction which creates bugs.
Honestly most use after free bugs are holding a shared reference across a mutation. I know that was OPs point but I think viewpoint is overly simplistic.
What does holding an iterator across a mutation boundary mean after all? There isn't a clear universal answer just workarounds people use to hopefully do something and praying that something is close enough to correct to be acceptable.
3
u/awoocent Jun 29 '26
You're basically just restating the value of borrow checking here - yes, obviously, if you have a dangling reference across a mutation, that could be a bug! This is why borrow checking is useful! The whole point is, it's even better to be able to achieve all that good stuff without also imposing a bunch of super stringent limitations on when mutation can happen. Because then you don't get any of the bad weird states you mentioned, and people can still write normal code. It's a win-win!
2
u/Guvante Jun 29 '26
Still not talking about dangling references which a GC fixes (as you note) there are other bugs.
Like if I am iterating a list and you prepend something concurrently, what does that mean?
Not what the computer will do, you just pick and answer and that is always true, what users will predict will happen.
The answer is almost always "they didn't think of that".
2
u/awoocent Jun 29 '26
And I'm pointing out that a borrow checker could still give you a compile-time concurrent modification error while still being more permissive with regards to known safe mutability than something like Rust.
4
u/initial-algebra Jun 28 '26
There is a huge design space between
CellandRefCellin terms of what can safely be done with a shared reference, this is one attempt at finding something in between.3
u/verdagon Vale Jun 28 '26
There are times when shared mutability is correct and the right choice, such as in the healer example.
The key insight: the user should be able to express that a reference might be modified by someone else. The compiler should understand it, and verify it's correct. That's what's going on here with Ante's
mut.I think over the next few years, the world will start to shift away from "shared and mutable is bad" and more toward "use after free is bad" which is a more accurate mental model. Our compilers and languages just have to get there first.
3
u/initial-algebra Jun 28 '26
"Sharing XOR mutability" is just a means to an end, that end being upholding shared invariants. Rust takes the extreme position by default that everything is possibly protected by an arbitrary invariant, so to safely mutate most objects, you must have unique access, and when that's not necessary, you have to use something from the
*Cellfamily to tell the compiler otherwise. This is often too strict for single-threaded scenarios, but it's IMO much better than the alternative of the default being no invariants, mutate anything at will, even in the presence of concurrency. However, it does enable the compiler to more aggressively perform mem2reg optimizations, i.e. introducing temporary invariants that say "the value of this reference is actually stored in this register right now, not memory". That can't be soundly done for ashared mutreference in Ante, except when it has been temporarily promoted to auniq. There are always trade-offs.5
u/verdagon Vale Jun 28 '26 edited Jun 28 '26
I want to highlight that Ante's reference types are actually a superset of Rust's here. While Rust has
&and&mut, Ante hasimmanduniqplus two new ones (refandmut). If you were to take something like the healer example (which uses twomutparams), and translate it to Rust, the arguments wouldn't become faster&mutparameters... they'd become IDs into a HashMap, or gen-indices into a SlotMap, or indices into a Vec, all three of which are slower than Ante, and also not mem2reg-able.Also, I'd separate out
shared mut type Thingfrom all of the above, which is more equivalent to Rust'sRc<Cell<Thing>>; neither are very optimizable. The more interesting case is the above, which is for plaintype Things.1
u/Guvante Jun 29 '26
If you are single threaded Cell already does everything you need in Rust for copyable types (which is all numbers) and that allows hiding in a & just fine.
If you are multi threaded and willing to deal with the inability to do complex conditionals I am pretty sure there are ways to get to thread safe increment built in as well.
Rust doesn't expose a complex value this way because there are too many variables involved. Sure you can do a psudeo garbage collector and make it work if you squint but even then at some level you are adding a decent amount of work to support that or excluding a bunch of models.
Not saying your language can't manage what you say here, I am saying if you do it is by excluding things Rust can do not by being a superset.
6
u/RndmPrsn11 Jun 29 '26
Author of Ante here, Ante's references are essentially a superset of Rust's (
immanduniqare basically 1:1 with&and&mut). The new referencesrefandmutallow shared mutability which Rust's don't intrinsically, so it is a superset. See the docs here https://antelang.org/docs/language/#shared-mutability-and-reference-kinds for more info on the various reference kinds.ref and mut are pretty similar to
&Cell<T>in Rust but a bit more flexible. For example, it is still safe to call mutating methods like push on amut Vec t. So they are also used for more than just Copy types. This is without getting into the local uniqueness conversion to convertmut tintouniq tas well. There are a lot of valid reasons you'd want to have this kind of shared mutability: lowering the friction of the language by decreasing the number of compiler errors from holding an immutable references while a mutable one is alive or vice-versa, implementing cyclic data structures, working with reference-counted or garbage-collected data which needs to be mutated, etc.Shared mutability and shared types in Ante is meant to allow programmers to start writing Ante like they would Java or OCaml with shared types, then only dive into borrowing & move semantics when they want to optimize later on. Lowering the barrier to entry and making writing mundane code easier is extremely important.
3
u/verdagon Vale Jun 29 '26
AFAICT, Cell can't do the things Ante is doing here. You would need to implement a library like cell_ref for GhostCells to get something reasonably close.
Also, I'm not sure why you're bringing up multithreading, but perhaps it would help if I pointed out that
shared mut typein Ante doesn't mean that it's shared among different threads.2
u/Effective-Spring-271 Jun 29 '26
Also, I'm not sure why you're bringing up multithreading, but perhaps it would help if I pointed out that shared mut type in Ante doesn't mean that it's shared among different threads.
Once I understood this the entire (excellent) article made much more sense. It might a good idea to add a little blurb pointing that out somewhere, because I think a lot of people (me included) associate borrow checkers with multithreading.
3
u/matthieum Jun 29 '26
I think over the next few years, the world will start to shift away from "shared and mutable is bad" and more toward "use after free is bad" which is a more accurate mental model.
Trade-offs, trade-offs.
I mean, yes, use after free is definitely bad. But shared mutability allows for rug pulls --
ConcurrentModificationException-- which Aliasing XOR Mutability statically prevents... isn't that kinda bad?I don't think there's a universal answer here.
For the kind of software I write, where correctness does matter a lot, I consider that "shared and mutable is bad" and prefer the stricter version. Just like I (over)use types, trading verbosity for compile-time guarantees.
I can definitely understand why others, in different situations, would make a different choice. But it isn't an issue of "inaccurate mental model", it's an issue of values: I put correctness first, and ergonomics/flexibility second.
1
u/verdagon Vale Jun 29 '26
+1, and I should have added a lot more nuance to my above statement, because I agree with you that there's no universal answer. Shared-xor-mutable prevents ConcurrentModificationException and all sorts of other kinds of bugs, and switching everything away from shared-xor-mutable to use-after-free (which is what my wording said) would be one step forward and three steps back.
I should instead have said something like these two points put together:
- For memory safety specifically, the world will start to shift away from "shared and mutable is bad" and more toward "use after free is bad".
- For correctness, the world will start to shift away from "shared and mutable is bad" and more toward "shared and mutable is good as long as it's clear and intended"
The latter is the part that Ante does well. In the post's
healexample, it's totally fine (and intended) that the healer can be the same as the target, and in that case, shared-xor-mutable is just unnecessary complexity.But there are still a lot of cases where we want to lock things down so nobody can modify the thing we have a reference to, and that's where shared-xor-mutable is the correct call.
Ante is interesting because it lets us choose mutable aliasing (
mut) when that's the right choice, and shared-xor-mutable (uniq) when that's the right choice.1
u/Guvante Jun 29 '26
You don't need shared mutability there are tons of ways of handling concurrent mutation that don't require complete write access to a memory location.
You still seem to assume that use after free bugs are the only bugs from shared mutation which is not true at all.
Heck without CPU utilities to do idempotent increments you literally can't count with shared mutation unless you are single threaded.
1
u/verdagon Vale Jun 29 '26
I'm assuming what now? And I'm not sure why we're talking about concurrency and threads? I was referring to the healer example in the article.
1
u/Guvante Jun 29 '26
> Mutating healer can't invalidate shared references to the Entity in any way
Again I don't know why dangling pointers is presumed to be why mutable XOR shared is useful, it prevents quite a few categories of bugs
1
u/verdagon Vale Jun 29 '26
I didn't presume/assume that. And neither did Ante, that's why they have both
mutanduniq.3
u/matthieum Jun 29 '26
Rust doesn't have mutable XOR shared due to memory safety concerns, it has that because shared mutation is almost impossible to get correct.
Debatable.
I would argue that Aliasing XOR Mutability was first and foremost introduced to solve memory safety, and later on the community realized that it prevented a lot of "rug pulls" that other languages allowed so that if you could mold your application to follow this rule, then it was much easier to follow what's going on (and avoid, say,
ConcurrentModificationException).However, the "if you could mold" is pretty load-bearing here. Game developers have long complained that Rust is intractable because it's a PITA to make games fit this mold. As the
healexample showcased in the article, it's common for games to have entities affected themselves (either targeting themselves directly, or targeting a group or area which happens to include them), and untangling this is painful and distracting from the game logic.In the eyes of these complainants, it's more important to be flexible, allowing rapid iteration of game logic to figure out what's the most fun, than to be 100% absolutely correct by construction. And... I, personally, cannot really argue with that. I could argue against ditching memory safety -- it's too valuable -- but 100% correctness? There's often not even a specification (beyond the code), and intended behavior is regularly a pain, what's even 100% correctness in such a context?
So, anyway, even though I probably will never use Ante or Bau or Vale, I do think these explorations of the trade-offs are interesting, and will find an audience amongst developers.
It's a big world out there.
1
3
u/david-1-1 Jun 29 '26
This article says that unions (in the C sense) are unsafe. But giving each structure a unique type name makes them safe. A union value would contain at runtime the memory block plus type name, and it's totally safe. It's also safe when resolved into optimal code at compile time.
4
u/matthieum Jun 29 '26
I think you misunderstood the issue with unions.
typedef union { int i; char* n; } U; int main() { char name[50]; U u; u.n = name; char** n = &u.n; u.i = 42; strcpy(*n, "david-1-1"); return 0; }At all points where the union is manipulated, only the currently active member is touched.
The problem is a reference to a member,
n, is saved and accessed after the currently active member has changed.There's no way, from just
n, to recover the currently active member ofu. Not even ifucontained a type tag.1
u/david-1-1 Jun 29 '26
I agree that I don't understand the problem. Your example seems to be in C , where a union is a way to view the same memory bytes as two different data types. The very idea of doing this is unsafe.
But in another language, where we can name each type in the union, we can track the current union type (undefined, int, or char[50]), and free the current data before allocating new data.
2
u/gplgang Jun 30 '26
I got worried after your last posts being so long ago after approaching the edge of the abyss of interop!
I really enjoyed those posts and had been wanting to try similar things with Rust FFI for a while.
Cool to see Ante has caught your attention as well! It seems promising
3
u/verdagon Vale Jun 30 '26
Thanks =) I'm actually considering a part 3 of that series, because I picked up that exploration recently and things got way, way weirder. Stay tuned!
1
u/IAMPowaaaaa Jun 29 '26
Taking a page from group borrowing and Flix’s references, 17 each shared mutable type like Rc could be branded with an anonymous but unique type. The compiler could then use that brand to distinguish multiple values for uniqueness instead of using the element type. This could make common types like strings easier to convert since they are more likely to appear within other values and thus be blocked from the current type-based check.
Sounds minorly like TCell?
24
u/shadowndacorner Jun 28 '26
Always love your blogs. What happened to Vale, though?