Languages ∩ Architecture

In The Golden Spike, and Resurrecting the Vale(n) Programming Language, we saw how it was possible for a language to call Rust functions directly, without going through C, by directly integrating with the Rust compiler. 0 1

It was one hell of a journey!

At the end, I managed to make the "golden spike", a Valen program that uses a Rust graphics library:

In there, I hinted about how we're doing borrow checking between Valen and Rust:

This post is long so I'll save the details for the next one, but TL;DR: it's not complete yet, but this prototype is doing some borrow checking over the boundary!

Welcome to that next post!

In this post, I'll show you how we can uphold memory safety across the Valen/Rust boundary.

Plus, we'll see a certain surprise that happened as I was building it, which led to me breaking a foundational law of the universe.

Valen

(If you read the Golden Spike, skip this section)

This is all done in the new Valen programming language (successor to Vale).

It's a very early, experimental language that: 2

  • Calls into Rust functions (including generic ones!) seamlessly without bindings or wrappers.
  • Uses a more flexible borrow checker without shared-xor-mutable.
  • Has linear types for resource safety and Higher RAII.

It also has plans to add:

  • Zig-style comptime, so we can do things like the impossible optimization.
  • Generational references 3
  • An Rc that can hold mutable data without RefCell/Cell/Mutex.
  • Plus much more. 4
This entire endeavor is extremely experimental and many things have holes and sharp edges (see side-note 5). It's going to be a glorious few months of cleaning, rewriting, and solidifying this horror before I unleash it on the world. Feel free to check out the Valen repo and beware, here be dragons! 6

One could call it a Rust++, since it can seamlessly call into existing Rust code.

Or it could be more of a Rust--, because it aims to be simpler, andoffers an easier way to use Rust code.

We'll see some of that further below, when it lets us use existing Rust code in a more flexible way than Rust itself.

Let's talk about Valen's memory safety approach!

Valen's Memory Safety: Group Borrowing

This is something I'm ridiculously excited about for Valen.

At long last, Valen has an implementation of Nick Smith's Group Borrowing memory safety approach.

Group borrowing is similar to Rust's borrow checking, but without the shared-xor-mutable restriction.

This means that Valen functions can do things like:

  • Have local variables that point to the same object, and write through any of them.
  • Take multiple parameters that point to the same object, and write through them.
  • Take a parameter pointing at an object, and another parameter pointing somewhere inside that object, and write only the latter.

This comes with a lot of simplifying powers.

For example, here's a Rust function, and below I'll show what Valen can simplify it to.

rs
// Make attacker_id entity attack defender_id entity.
// Note: Attacker might be defender (attacking self).
fn attack(
    entities: &mut SlotMap<DefaultKey, Entity>,
    attacker_id: DefaultKey,
    defender_id: DefaultKey
) -> Result<(), String> {
    let a = entities
        .get(attacker_id)
        .ok_or_else(|| "Attacker not found in entities map".to_string())?;
    let d = entities
        .get(defender_id)
        .ok_or_else(|| "Defender not found in entities map".to_string())?;

    let a_energy_cost = a.calculate_attack_cost(d);
    let d_energy_cost = d.calculate_defend_cost(a);
    let damage = a.calculate_damage(d);

    let a_mut = entities
        .get_mut(attacker_id)
        .ok_or_else(|| "Attacker not found in entities map".to_string())?;
    a_mut.use_energy(a_energy_cost);

    let d_mut = entities
        .get_mut(defender_id)
        .ok_or_else(|| "Defender not found in entities map".to_string())?;
    d_mut.use_energy(d_energy_cost);
    d_mut.damage(damage);

    Ok(())
}

The above becomes this Valen function: 7

valen
// Make attacker_id entity attack defender_id entity.
// Note: Attacker might be defender (attacking self).
func attack(a &Entity in g, d &Entity in g) mut(g) {
  let a_energy_cost = a.calculate_attack_cost(d);
  let d_energy_cost = d.calculate_defend_cost(a);
  let damage = a.calculate_damage(d);
  a.use_energy(a_energy_cost);
  d.use_energy(d_energy_cost);
  d.damage(damage);
}

A huge difference! 8 9

This is possible because group borrowing lets us have multiple mutable references to the same object.

This is what the Rust code was trying to express, but Rust's borrow checker doesn't allow it. 10

Specifically, Rust's borrow checker has the shared-xor-mutable restriction, which says:

  • Each reference must be read-only or read-write. 11
  • If you have a read-write reference to an object, you can't use any other references to it.
  • If you have a read-only reference to an object, you can't write through it. 12

Group borrowing instead tells the compiler when two references might be pointing at the same thing, and then uses invalidation tracking to make sure that we don't use a reference that might be dangling. 13

You can almost think of it like a compile-time version of generational references (in fact, that's close to how it's implemented in the compiler!)

If you want to know more about group borrowing, check out the original group borrowing post.

For now, just know that it's more flexible because it allows multiple references to an object, and lets us write through any of them.

Now for the surprising part: Valen can also do that with Rust objects.

In other words, we can have multiple mutable references to Rust objects, safely.

I can hear some of you going, "Wait, what?!"

Side Notes
(interesting tangential thoughts)
0

0% of this article was written by AI. More thoughts here.

My stance: if you want someone to take the time to read something, take the time to write it by hand.

Thanks for reading =)

1

Also thanks to Let's Get Rusty, who made a youtube video on the Golden Spike article. It's a great overview, check it out!

And one correction: I don't work for Mojo anymore. They definitely wouldn't have let me do any of this!

2

The main differences between Valen and Vale:

  • Valen will have group borrowing, Vale had region borrowing.
  • Valen will have normal generational references, Vale had probabilistic generational references.
  • Valen will have (and Vale didn't have) Rust interop, Rc, comptime.
  • Valen won't have (and Vale did have) perfect replayability, fearless FFI.

Both have linear types.

Their syntax is still mostly the same, but Valen might change to be more of a "simpler Rust" syntactically.

3

The normal ones, not the Vale-style random generational references.

4

Things like: a better async story, better enums, better mustprogress optimizations, closures implementing traits, universal function call syntax, good compile times, and maybe if we're lucky we can bring back some subset of perfect replayability.

5

To be clear about what works today:

  • Valen has linear types, but we can't yet declare that an existing Rust type is linear.
  • Valen has group borrowing (except for closures), and it borrow checks across the boundary.
  • Structs work across the boundary, even Valen structs that implement Rust traits. But they must be zero-sized (filled structs work in my separate prototype, not yet in Valen).
  • Generational references are temporarily disabled, hopefully coming back soon.
6

Green dragons, specifically.

7

The biggest takeaway is that the in g on the references tells the compiler that the references might point at the same object. The mut(g) also says that we might mutate it. To really know what's going on here, check out the Group Borrowing article.

8

Also, if you look very closely, you'll see that the Valen example is probably faster, because it doesn't need all the error-handling branches, and instead of doing 4 lookups like the Rust version, it's doing zero (or two, because we might have just moved them out to the caller, depending on circumstances).

Take that with a grain of salt though, I haven't benchmarked it yet. If true, that'll be a wild post when it comes.

9

I like this example because it's the clearest, but I've come across more common cases where group borrowing can make something simpler than normal borrow checking. See this section for more examples.

10

It might seem like we could do this with Cell, but that makes a lot of assumptions about what's inside these methods. Depending on how complicated use_energy and damage are, using a Cell might not be such a great idea. GhostCell might be a better option for this example. There are other group-borrowing examples that GhostCell wouldn't quite work for though, I wouldn't consider it equivalent.

11

More specifically, a reference must be "shared" or "unique".

12

We can technically still mutate through a Rust shared reference, if it has an UnsafeCell somewhere inside it. This usually comes with a tradeoff, such as not being able to make references to some things inside it (Cell), run-time errors (RefCell) or unsafe.

13

"Dangling" means pointing at something that has already been destroyed.

Breaking a foundational law of the universe

That's right, Valen code can safely have multiple mutable references to Rust objects.

The Rust half of my brain can't even comprehend how blasphemous this is. 14 The mad sorcerer half laughs at the Rust half's discomfort. 15

And yet, it works!

valen
import slotlib.Slot;

exported func main() int {
  let slot = Slot.new();
  let ref_a = &slot;
  let ref_b = &slot;
  ref_a.mutate(42);
  ref_b.mutate(73);
  return ref_a.get();
}

rs
pub struct Slot {
  value: i32,
}
impl Slot {
  pub fn new() -> Slot { ... }
  pub fn get(&self) -> i32 { self.value }
  pub fn mutate(&mut self, x: i32) {
    self.value = x;
  }
}

Note how ref_a and ref_b are both pointing to the same Slot, yet ref_a.mutate(42); is handing one of those references into a Rust &mut reference.

But to understand why that works, let's first talk about temporarily unique references and "noalias", and then I'll explain how they help us call into Rust functions like we did above.

14

If you've used Rust before, this is surprising because one of the central tenets of Rust is that there can only be one mutable reference to an object at a time.

But we all knew that that was an imprecise model, that's what people mean when they say that Rust's borrow checker is conservative.

(Don't get me wrong, group borrowing is also conservative, just not as much as Rust in this case)

15

And the third half laughs at how the first two are always bickering over what's possible and/or sane.

Temporary Uniqueness and noalias

A related question I'm often asked is:

"Wait, Verdagon, Rust references are more optimizable because they become noalias pointers in LLVM. If Valen references can alias each other, does that make it slower?"

A good question, and understanding the answer also helps us understand the Valen/Rust boundary.

TL;DR: Valen uses noalias too, so it's just as fast.

I'll explain the question a bit more, and then explain how Valen does it.

What is noalias?

In Rust, when you have a reference parameter pointing at some data, 16 you know that nobody else is writing to that data. rustc makes those into LLVM noalias pointers, so LLVM knows about that, and can optimize better.

This is similar to C's restrict pointers, which also become LLVM noalias pointers.

For example, here's a C function without restrict:

c
void addMuchRandom(int *destination, struct Rand *random_state) {
    for (int i = 0; i < 100; i++)
        *destination += next_random(random_state);
}

LLVM can't optimize this well. 17

However, if we make that parameter int *restrict destination, like this:

c
void addMuchRandom(int *restrict destination, struct Rand *random_state) {
    for (int i = 0; i < 100; i++)
        *destination += next_random(random_state);
}

...then it can be optimized better, because the C compiler makes it into an LLVM noalias pointer. 18

Now LLVM can optimize the function to something like this: 19 20

c
void addMuchRandom(int *restrict destination, struct Rand *random_state) {
    register int temp_register = *destination;
    for (int i = 0; i < 100; i++)
        temp_register += next_random(random_state);
    *destination = temp_register;
}

This is faster because we load from the pointer once at the beginning and store into it once at the end, instead of in every iteration.

So C restrict pointers are a lot like Rust references. With both, nobody else is writing to that data, and both become LLVM noalias pointers, which are optimized better.

16

Note this happens for shared references to Freeze data (which means it doesn't contain any UnsafeCell, Cell, RefCell, etc.) or unique references.

17

It becomes this unoptimized LLVM IR, which optimizes to this optimized LLVM IR, which isn't very optimal, because we're still doing a load and store in every iteration.

18

It becomes this LLVM IR with noalias which, when optimized, becomes this more optimal

19

With restrict, LLVM can lift the loads and stores out of the loop.

Before, without restrict, LLVM knows that that int* destination might be pointing anywhere... including into random_state->state_int, in which case lifting the loads and stores would cause weird behavior.

restrict promises LLVM that it won't be called like that.

20

This isn't exactly how the register keyword works in C, but you get the idea.

How does Valen make LLVM noalias pointers?

Now let's look at our example Valen function again, plus the damage method:

valen
func attack(a &Entity in g, d &Entity in g) mut(g) {
  ...
  d.damage(damage);
}

func damage(entity &Entity mut, amount int) {
  entity.hp = max(0, entity.hp - amount);
}

The question breaks down into three questions:

  1. Is damage's parameter entity marked noalias? (Hint: yes)
  2. Are attack's parameters a and d marked noalias? (Hint: no)
  3. Can the d.damage(damage) call hand a non-noalias reference (d) to a noalias parameter? (Hint: yes)

Q1: Is damage's parameter entity marked noalias?

Yep, it is.

When a reference is the only reference into its group, Valen makes it into an LLVM noalias pointer.

So in this damage function:

valen
func damage(entity &Entity mut, amount int) {
  entity.hp = max(0, entity.hp - amount);
}

...which is syntactic sugar for this:

valen
func damage<eg'>(entity &Entity in eg, amount int) mut(eg) {
  entity.hp = max(0, entity.hp - amount);
}

...Valen sees that entity is the only reference into the group eg.

Therefore, it knows that nobody else is writing to the data that this reference points at.

Therefore, it can be an LLVM noalias pointer.

Q2: Are attack's parameters a and d marked noalias?

Nope, they aren't. This is intentional, because they do alias. That's the user's intent, after all!

More specifically, looking at this attack function:

valen
func attack(a &Entity in g, d &Entity in g) mut(g) {

Valen sees that a isn't the only reference into the group g, so it can't be marked noalias.

Same with d.

Q3: Can the d.damage(damage) call hand a non-noalias reference (d) to a noalias parameter?

Quite easily!

This just works™ already in LLVM, because that's how we've been doing it in C for decades. For example this C program hands an aliased pointer into a restrict pointer parameter, and it's fine.

This is safe, as long as the data the pointer is pointing at isn't being accessed through any other argument.

This is also how Rust works under the hood! When a Rust type contains an UnsafeCell (or Cell, or RefCell, or GhostCell, etc.), shared references to it are normal LLVM pointers (no noalias). Then, usually through a safe API around an unsafe block, we can get a &mut reference (LLVM noalias pointer) out of it.

So, now that we know how Valen can make LLVM noalias pointers, let's finally see how that helps Valen call into Rust functions!

Memory safety across the boundary 21

Here's the Valen program from above, calling into a Rust library:

valen
import slotlib.Slot;

exported func main() int {
  let slot = Slot.new();
  let ref_a = &slot;
  let ref_b = &slot;
  ref_a.mutate(42);
  ref_b.mutate(73);
  return ref_a.get();
}

rs
pub struct Slot {
  value: i32,
}
impl Slot {
  pub fn new() -> Slot { ... }
  pub fn get(&self) -> i32 { self.value }
  pub fn mutate(&mut self, x: i32) {
    self.value = x;
  }
}

Note how ref_a and ref_b are both pointing to the same Slot, yet ref_a.mutate(42); is handing one of those references into a Rust &mut reference.

Like we saw above, this is safe because, for the duration of that mutate call, only one pointer to slot is being used.

Valen knows that at the callsite because only ref_a is being passed into ref_a.mutate(42).

You could almost think of it like Valen is making ref_a temporarily unique for the call.

And under the hood, we're passing a normal LLVM pointer (ref_a) into a function that receives it as an LLVM noalias pointer, which is fine for the same reasons this C program from above was fine.

21

Easter egg note!

Cricket-spitting is a sport where people spit a dead cricket as far as they can. Whoever can spit the cricket the furthest is declared the winner. Glorious!

(Source: Wikipedia)

A more advanced case

So far, things just seem to work for free, since group borrowing was so similar to Rust.

However, there was a case where group borrowing didn't really understand a reference that came from Rust.

To illustrate, here's an (unsafe) Valen program using a Rust method gem that it needs to understand:

valen
import box_lib.Chest;
import box_lib.Gem;
exported func main() int {
  let chest = Chest.new();
  let gem_ref = chest.gem();
  chest.replace(8);
  // Should error here
  return gem_ref.get();
}

rs
pub struct Gem {
  value: i32,
}
impl Gem {
  pub fn get(&self) -> i32 { self.value }
}

pub struct Chest {
  gem: Box<Gem>,
}
impl Chest {
  pub fn new() -> Chest { ... }
  pub fn gem(&self) -> &Gem { &self.gem }
  pub fn replace(&mut self, value: i32) {
    self.gem = Box::new(Gem { value });
  }
}

This Rust function was problematic for Valen:

rs
  pub fn gem(&self) -> &Gem { &self.gem }

...because Rust doesn't describe where inside self that Gem lives, and Valen definitely wants to know where every reference points.

Valen would love it if Rust said this:

rs
  pub fn gem(&self) -> &Gem in self.gem[] { &self.gem }

...but alas, we live in that world not.

So, without that information, group borrowing got confused in these lines:

valen
  // Where does gem_ref even point to?
  let gem_ref = chest.gem();
  // Modifies chest, but what does that do to gem_ref?
  chest.replace(8);
  // Is this line even safe?
  return gem_ref.get();

It was confused because Valen needed to know where a reference points, to know whether it's dangling.

So, Valen has to add a new concept on top of group borrowing.

Inspired by Vale's isolates, 22 I'm adding a concept called a wildcard descendant path.

Valen now sees the Rust function as if it was this:

rs
  pub fn gem(&self) -> &Gem in self... { &self.gem }

That self... means "somewhere inside self".

Now Valen sees the lines like this:

valen
  // gem_ref is a `&Gem in chest...`
  let gem_ref = chest.gem();
  // Modifies chest, so invalidates all refs that might point inside `chest`
  chest.replace(8);
  // Show an error on this line, because `&Gem in chest...` points inside `chest`
  return gem_ref.get();

And it works! It successfully gives a compile error on that line, because gem_ref.get() is trying to do a use-after-free.

To summarize, Valen's wildcard descendant paths let it understand references that point "somewhere" inside an object. 23

Quite serendipitously, it closed the gap between Valen and Rust.

With that, Valen's borrow checker could uphold safety across the boundary, and thus the second golden spike was complete.

Reminder: Even though all these examples worked, it doesn't yet mean that we've proven Valen to be completely memory safe when calling into Rust, or completely memory safe in general. Like I said above, this entire endeavor is extremely experimental and many things have holes and sharp edges. We will undoubtedly find problems, and hopefully also find fixes for them. Stay tuned!

That's all for now

Thanks for reading!

In the next few articles, I'll explain more about group borrowing, and how Valen's group borrow checker is implemented.

And maybe, if we're lucky, I'll have implemented a third golden spike, which has something to do with Rust and linear types.

This is just the tip of the iceberg, so stay tuned by subscribing to my RSS feed, r/valen, or joining the Valen discord. And follow me on Bluesky and Mastodon!

Cheers,

- Evan Ovadia

22

In Vale's design, you can open an isolate as a region, and then have a reference to anything inside that region. Also, Vale regions are un-typed; in other words, you can have a reference to type X that lives "somewhere inside" object Y's isolate's region.

23

This was actually already in my plans for Valen, because sometimes we don't want to put an entire path (like &Gem in self.gem[]) in a signature, and it's more readable to just say ... (like &Gem in self...).

FAQ

Returning &mut

Q: When we get a &mut from a Rust callee, isn't it UB to read/write through another reference pointing at the same data? For example:

valen
exported func main() int {
  let slot = Slot.new();
  let ref_a = &slot;
  let ref_b = &slot;
  // Is this &mut / noalias?
  let g = ref_a.get_mut();
  // If so, wouldn't this be UB?
  let x = ref_b.peek();
  return g.get() + x;
}

rs
pub struct Gem { value: i32 }
impl Gem {
  pub fn get(&self) -> i32 { self.value }
}
pub struct Slot { gem: Gem }
impl Slot {
  pub fn new() -> Slot { ... }
  pub fn get_mut(&mut self) -> &mut Gem {
    &mut self.gem
  }
  pub fn peek(&self) -> i32 { self.gem.value }
}

A: Nope. It would cause problems if Valen was generating Rust MIR that it handed to rustc, but Valen's not doing that. Valen generates LLVM IR directly 24 so the question is really, "Do the rustc-generated LLVM IR and the valenc-generated LLVM IR work together without UB?"

And they do work together, because of noalias's semantics.

In C terms, if a C main has two aliasing pointers, and hands one to a function foo that accepts it as a restrict pointer, and returns a pointer derived from that to main, main then receives it as an aliasing pointer. In other words, the restrict only applied to foo's body.

Why not transpile to Rust, or Rust MIR?

Transpiling to Rust wouldn't work, because Rust doesn't support things like comptime (which I really want for e.g. The Impossible Optimization).

Transpiling to Rust MIR would almost work, except it doesn't allow fine-grained aliasing information. In Rust MIR, you can specify that something is noalias or not. In LLVM however, you can specify that it's noalias or not or something between. Specifically, you can say that it aliases some things and not other things. In group-borrowing-speak, you can specify what group it's in. This is done via the !alias.scope metadata on loads/stores, and putting noalias metadata on call expressions (rather than just function parameters).

It's also slightly philosophical; while Valen is still experimental, it needs to be more decoupled from constraints, including Rust MIR. As Valen grows and stabilizes, I could see this changing.

One possible way forward would be:

  • Valen stabilizes, to know what's actually useful.
  • We propose adding !alias.scope/noalias metadata to Rust MIR.
  • We implement and upstream those changes to rustc.
  • We stop generating LLVM IR directly, and start generating MIR.

Why not just improve Rust? Why make another language?

I think Rust would benefit from adding some of Valen's features, like linear types.

However, I'm not certain yet that we should add group borrowing to Rust directly.

Rust's entire culture, ethos, and learning curve is based on a simple principle of shared-xor-mutable. It's unintuitive, but it's consistent enough that once you internalize it, it's a straightforward (if lengthy) path to master Rust.

Valen is all about mutable aliasing, which is the exact opposite. Valen is also consistent, making it a straightforward path to master.

However, if you blend the two wrong, you get an inconsistent mess that theoretically has the benefits of both, but in practice also makes Rust much harder to learn.

I could see a future where Valen pivots a few times, figures out how to do group borrowing well, and armed with that strong data point we can then consider how to add it to Rust. Until then, there's a lot of exploring to do.

Thoughts on Group Borrowing's Flexibility

Q: Any more examples, besides that func attack one?

A: Sure, here's some more: 25

  • Read-world-mutate-piece pattern: If we have a function that needs to read the world but modify only a part of it, normal borrow checking often forces us to either take the entire mutable world plus IDs into it, or split our data into separate ECS-esque tables. Group borrowing doesn't force that because it can express that in terms of references.
  • Pure results: In a game, if you have an NPC (immutably) weigh different "action possibility" structs to decide its ultimate action to execute, group borrowing lets those structs contain references, where normal borrow checking makes them store IDs.
  • RAII: Normal borrow checking doesn't allow for RAII "rollback structs", where a e.g. struct Rollbacker can have a mutable reference to a e.g. struct Transaction, because Rollbacker's &mut Transaction makes the Transaction unusable to everyone else.

Ask me in r/valen comments and I can expand on these if you want.

I have a feeling this is just the tip of the iceberg. When we release this to the world, we'll be seeing a lot more patterns.

Also, there will definitely be an article on this soon, stay tuned!

24

Though, Valen does give the generated LLVM IR into rustc at the very last second to sneak it into rustc's invocations of LLVM's optimization and its linking.

25

...plus a lot more that enable reference counting and generational references, but I don't want to go too deeply into that yet.