Safe Made Easy Pt.2: Don't Fear the Ref

ergeysay1 pts0 comments

Safe Made Easy Pt.2: Don't Fear the Ref | Of Rabbits and HolesSafe Made Easy Pt.2: Don't Fear the Ref<br>2026-06-03<br>Intro<br>Motivating example<br>The solution<br>But what about self-references?<br>States of aggregation<br>Shattering the cycle<br>Behind the scenes<br>The rules so far<br>Conclusion<br>Intro<br>So, in the first installment of the series I proposed a flow-sensitive typing solution to safety.<br>To recap, in this model each instance of an owned type is guaranteed to be dropped only once.<br>Which brings the question: how do we share access to an instance of an owned type?<br>Option A is simple - we don&rsquo;t. There is no sharing, you can only move instances between owners. Solved. Except this would be far too strict - we will have to move instances all the time to do something interesting or useful. But I can imagine a language like that and it can be a viable solution, for some definition of.

Option B is unrestricted sharing. There is an instance, we take a pointer to it, we don&rsquo;t know or care what is happening with the instance. If it&rsquo;s dropped and we still use it - oops, bad things happen but that&rsquo;s life. This is what C and C++ offer. C++&rsquo;s references kinda try to sort this, but fall flat because the language doesn&rsquo;t really go to an extent of proving that the referenced object still exists while being accessible. For example, nothing stops you from doing this:<br>struct Foo {<br>int i;<br>};

Foo *foo = new Foo { .i = 123 };<br>int& ref = foo->i;<br>delete foo;<br>printf("%d\n", ref);<br>Apart from that, we don&rsquo;t know how many references there are - in other words, we have unrestricted aliasing.

Option C is shared_ptr / Rc - which kind of is a natural evolution of the option B. It solves the issue &ldquo;we don&rsquo;t know if the referenced object is still alive&rdquo; with a tradeoff - now the references we have guarantee the object is still alive by definition, and the object will live as long as it is reachable. Solved. Now we introduced shared ownership with all that entails - we don&rsquo;t know who owns a particular object (but we know how many owners it has).

Option D is tracing GC, which I think can be seen as an evolution of option C to an extent? As in, in option C we trace dead objects incrementally, whereas a GC traces live objects. Again, shared ownership, just with a different set of tradeoffs.

But what if there was a way to check - statically, at a compile time - the validity of a reference? Is it definitely valid - and then you can use it as you please, definitely invalid and you need to re-take it, or possibly invalid - in which case you have to confirm at runtime if the referenced object is still there or not?<br>This is what this post is about. Here, I will introduce a dependency graph linking values at compile time, and the operations we can perform on it. I will also demonstrate how the same mechanism will allow us to detect and prevent ownership cycles at compile time and uphold the single acyclic ownership guarantee, freeing us from memory leaks.<br>It&rsquo;s also one of the most complex parts of the entire series, so buckle up, we&rsquo;re going for a ride.<br>Motivating example<br>An important note : while I will still use pseudo-code to illustrate my points, I will try and gradually make it more and more real starting with this post. So don&rsquo;t worry if you see something strange or unfamiliar - it all will come together at the end.<br>So, we start with the following problem: nothing can be shared.<br>var a: T = ...<br>var b = a // Moves from `a` unconditionallyWe want to introduce a type that will allow us to alias a value without taking ownership:<br>var a: T = ...<br>var ref: Ref = &a<br>// Now we can use `ref` as a synonym for `a`But what if a ceases to be available?<br>var a: T = ...<br>var ref: Ref = &a<br>a.Destroy()<br>// What happens to `ref` here?The solution<br>At compile-time we track which value ref is actually pointing to. If the original value is invalidated, the ref cannot be used anymore. Invalidation does not necessarily mean drop or Destroy() or something destructive - a relocation of an object is inherently invalidating since all references pointing to it are stale after relocation.<br>var a: T = ...<br>var ref: Ref = &a<br>a.Destroy()<br>io.Print("${ref}") // Compile-time error: cannot use invalid reference `ref`Done. Unless you allocate another object and point the now-stale ref to that object, you cannot use it anymore.<br>var a: T = ...<br>var ref: Ref = &a<br>a.Destroy()<br>var b: T = ...<br>ref = &b<br>io.Print("${ref}") // Works, `ref` is known to be valid at this pointBut then you ask - wait, how would that work in presence of branching control flow? What happens if we cannot statically, at compile-time, know which object the reference is pointing to?<br>var a: T = ...<br>var b: T = ...

var ref = &a<br>if Math.Random() % 2 == 0 {<br>ref = &b<br>a.Destroy()<br>// Is ref still valid here or not?And that&rsquo;s actually a very good question! With a very simple answer. If we don&rsquo;t know which object a reference is depending on, it means it...

rsquo object option time know still

Related Articles