{"slug": "a-new-way-to-think-about-borrow-checking-in-reverse", "title": "A new way to think about borrow checking (in reverse)", "summary": "The Valen programming language's borrow checker runs its memory-safety check in reverse, walking a function from its last statement back to the top, according to a blog post by the Valen project. Valen compiles functions in four phases — static typing, static grouping, and a reverse safety check among them — plus a secret fifth phase that computes LLVM attributes describing which operations may mutate which groups. The author says Valen is the only borrow checker whose safety check goes backwards, a design chosen for convenience, loops and speed, and that it supports optional shared-xor-mutable checking and memory-safe calls into Rust functions.", "body_md": "Have you ever had a weird idea, and then it worked, but you can't really believe it?\n\nThat's what I'm feeling right now, about Valen's borrow checker.\n\nBecause it **travels backward through time.**\n\nOr, to be less dramatic, it **walks functions backwards,** which is still *very* weird.\n\nYears ago, if you told me it would be good to iterate a function in reverse, I would have called you crazy.\n\nIt turns out, there's actually some really good reasons to do this, and it's fairly common when you're doing e.g. optimization passes. [0](#note0) But I don't think anyone's done this for checking memory safety.\n\nIf you want a really deep look into the world of borrow checking, read on!\n\n*We're about to see some weird things.* [1](#note1)\n\nOr [destructor insertion](https://www.youtube.com/watch?v=IpuvQUVB8Cg), and now, checking memory safety!\n\n 0% of this article was written by AI. [More thoughts here](https://verdagon.dev/blog/personal-ai-policy).\n\nMy stance: if you want someone to take the time to read something, take the time to write it by hand.\n\nThanks for reading =)\n\nThis isn't going to be your normal shared-xor-mutable borrow checking.\n\nThis is [Valen](https://github.com/valen-lang/valen)'s borrow checker, the one that [makes shared-xor-mutable optional](https://verdagon.dev/blog/valen-group-borrowing), and can [call into Rust functions](https://verdagon.dev/blog/golden-spike-reviving-vale-valen) with [memory safety across the boundary](https://verdagon.dev/blog/boundary-memory-safety).\n\nIt's kind of like Rust's borrow checker, but it also has memory-safe mutable aliasing. If you want to know more about that, check out [this post](https://verdagon.dev/blog/valen-group-borrowing), though you won't need to understand that for most of this post.\n\nGreen dragons, specifically.\n\nValen compiles a function in four phases: [3](#note3)\n\nI'll explain all of this further below.\n\n**The weirdest part is that #4 happens in reverse.** It looks at the last statement in the function, and then walks backwards until it reaches the top of the function.\n\nThis is weird because, as far as I know, it's the **only borrow checker whose safety check goes backwards.** [5](#note5)\n\nFurther below, I'll explain why we're going backwards (hint: convenience, loops, and speed).\n\nBut first, let's see **how it works,** and what it even *means* to go backwards.\n\nLet's skip phase 1, and start with phase 2.\n\nThere's also a secret fifth phase, where we calculate what attributes to send to LLVM to describe what operations might change what groups. But that's out of scope for this article.\n\nIt also figures out all the groups invalidated by any mut effects from any instructions.\n\nFor example:\n\nThe static typing pass will be familiar to some of you, it assigns types to variables.\n\nFor example, it turns this program:\n\n``` js\nexported func main() {\n  let fuel = 5;\n  let force = fuel;\n}\n```\n\ninto this program:\n\n``` js\nexported func main() {\n  let fuel i64 = 5;\n  let force i64 = fuel;\n}\n```\n\nThis is pretty normal, lots of compilers do it.\n\nCompilers usually do this as a **forward** pass, walking from the top of the function to the bottom, figuring out types as it goes.\n\nWe're going to do something *similar but weirder* in phase 3; phase 3 is going to do **static grouping.**\n\nThis is another forward pass, which figures out where each reference points to.\n\nFor example, in this program:\n\n``` js\nfunc print_first_fuel_cell(s &Ship) {\n  let first_ref = &s.fuel_cells[0];\n  print_int(first_ref);\n}\n```\n\nStatic grouping figures out that first_ref is pointing in s.fuel_cells[]: [7](#note7)\n\n```\nfunc print_first_fuel_cell(s &Ship) {\n  // Static typing says:   first_ref &int\n  // Static grouping says: first_ref &int in s.fuel_cells[]\n  let first_ref = &s.fuel_cells[0];\n  print_int(first_ref);\n}\n```\n\nIn other words, it's pointing to an int somewhere in the s.fuel_cells[] array.\n\nThis information is now *part of the variable's type.*\n\nWe'll be using this information in the next phase, which actually checks for use-after-free.\n\nI'll probably start moving away from the term \"group\" soon, switching to \"path\" instead. Opinions welcome!\n\n In V1's static grouping phase, it would also figure out *when* a reference was made. So for example, instead of &int in s.fuel_cells[], it would know that it was &int in s.fuel_cells[] from line 7.2. This was pretty awesome, we were literally **smuggling time and space** information into a variable's type. This new V2 version doesn't need to do that because it does a backwards scan, as we'll see next.\n\nNow we can use everything Phase 3 calculated!\n\nThis is the phase that **checks for use-after-free.**\n\nIt does it with a **backwards scan,** *as was foretold.*\n\nLet's see it happen in a slightly bigger example:\n\n```\nstruct Entity { hp int; buffs []int; }\nfunc add_buff(self &Entity mut) { ... }\n\nfunc foo(e &Entity mut) {\n  let buff = &e.buffs[0];\n  e.add_buff();\n  print_int(buff);\n}\n```\n\nThis program is doing a **use after free,** and we want the compiler to complain.\n\nFirst, I'll desugar it into a more explicit syntax:\n\n```\nstruct Entity { hp int; buffs []int; }\nfunc add_buff<g'>(self &Entity in g) mut(g) { ... }\n\nfunc foo<g'>(e &Entity in g) mut(g) {\n  let buff = &e.buffs[0];\n  e.add_buff();\n  print_int(buff);\n}\n```\n\nNow I'm going to add in some comments, all determined by the backwards scan as it goes.\n\nFurther below, I'll explain what \"DECLARE\", \"USE\", and \"MUTATE\" mean.\n\n```\nstruct Entity { hp int; buffs []int; }\nfunc add_buff<g'>(self &Entity in g) mut(g) { ... }\n\nfunc foo<g'>(\n  e &Entity in g          // DECLARE e (group g)\n) mut(g) {\n  let buff = &e.buffs[0]; // DECLARE buff (group g.buffs[])\n  e.add_buff();           // USE e (group g)\n                          // MUTATE group g\n  print_int(buff);        // USE buff (group g.buffs[])\n}\n```\n\nThe algorithm walks **in reverse,** from the end of the function to the start.\n\nAs it scans upward, it looks for these events:\n\npending contains all of the things USEd below, that we hope to see DECLAREd above.\n\nWe hope to never see a MUTATE to anything pending.\n\nTo really understand it, let's see it in action.\n\nFirst, we **look at the last line:**\n\n```\n  print_int(buff);        // USE buff (group g.buffs[])\n```\n\nThis is a USE, so we add it to the pending list. pending now has:\n\nNow, still going backwards, we encounter the line above it:\n\n```\n  e.add_buff();           // USE e (group g)\n                          // MUTATE group g\n```\n\nWe see the MUTATE so we **ensure no variables in pending are invalidated.**\n\nSpecifically, we'll ensure that nothing in pending is pointing at anything under e.\n\nHere's what just happened: buff was pointing at g.buffs[], which starts with the g we just mutated with add_buff. That means that buff is **invalidated above the line where it's used,** which is a use-after-free.\n\nSo the compiler shows an error here. [Huzzah!](https://www.youtube.com/watch?v=y13n1CeyqjY&t=132s)\n\nFirst person to break it gets a prize!\n\nLet's see a **correct program,** so we can see the backwards walk encountering a declaration.\n\nHere's the same program, but with e.add_buff(); moved up, to be the first line in the function.\n\n```\nstruct Entity { hp int; buffs []int; }\nfunc add_buff<g'>(self &Entity in g) mut(g) { ... }\n\nfunc foo<g'>(\n  e &Entity in g          // DECLARE e (group g)\n) mut(g) {\n  e.add_buff();           // USE e (group g)\n                          // MUTATE group g\n  let buff = &e.buffs[0]; // DECLARE buff (group g.buffs[])\n  print_int(buff);        // USE buff (group g.buffs[])\n}\n```\n\nLike before, we start at the end, and get to this line:\n\n```\n  print_int(buff);        // USE buff (group g.buffs[])\n```\n\n...and see that it's a USE so we add buff to the pending list:\n\nNow we go one line upward, to this line:\n\n``` js\n  let buff = &e.buffs[0]; // DECLARE buff (group g.buffs[])\n```\n\nThis DECLAREs buff, so we **remove** it from pending.\n\npending is now empty.\n\n(That means that **there are no variables that are used below/later in the function.**)\n\n```\n  e.add_buff();           // USE e (group g)\n                          // MUTATE group g\n```\n\nWe see the MUTATE, so we **ensure no variables in pending are invalidated.**\n\nThere are no variables in pending, so nothing interesting happens.\n\nThen we see it USE e, so we **add** it to pending:\n\nAnd that's the end of the function!\n\nWe made it all the way to the end without seeing any MUTATEing any pending variables, which means **the program is memory-safe.**\n\nThere are three reasons: convenience, loops, and speed.\n\nGenerally, going backwards is convenient for figuring out **when a thing is last used.**\n\nFor example, look at this function:\n\n``` js\nfunc attack(a &Entity in g, d &Entity in g) mut(g) {\n  let a_power = a.calculate_attack_power();\n  let a_energy_cost = a.calculate_attack_cost(d);\n  let d_armor = d.calculate_defense();\n  let d_energy_cost = d.calculate_defend_cost(a);\n  a.use_energy(a_energy_cost);\n  d.use_energy(d_energy_cost);\n  d.damage(a_power - d_armor);\n}\n```\n\nIf we want to do *bookkeeping X* when a is declared and do *bookkeeping Y* when a is no longer needed, [9](#note9) we have two options:\n\nNotice how the forward scan involved that extra fetch/navigate, because we couldn't recognize the last use when we encountered it.\n\nThe backward scan was simpler.\n\nGoing backwards is also better for loops.\n\nHere are a couple examples that I've been using to think about loops: [10](#note10)\n\nThis is a correct program, but it shows how tricky it is to know if that observe(e) is valid.\n\n``` js\nexported func main() {\n  let arr = ArrayList<int>(3);\n  let e = &arr[0];\n  \n  while ... {\n    observe(e); // Valid `e` use.\n    add_element(&arr);\n    set e = &arr[0];\n  }\n}\n```\n\nThat e actually can come from two different places:\n\nA forward scan encountering observe(e) would need to **look ahead** to the set e to conclude it's valid.\n\nThis is a similar example without the set e.\n\n``` js\nexported func main() {\n  let arr: ArrayList<int> = ...;\n  let e = &arr[0];\n\n  while ... {\n    observe(e); // Invalid `e` use.\n    add_element(&arr);\n\n  }\n}\n```\n\nWithout the set e, the e in the loop is invalidated by a *previous iteration* of the loop.\n\nIf we did a forward scan, we'd need to **look ahead** to see that there's no set e reviving e.\n\nThis is a tricky problem. One can:\n\nThe last one was tempting. Alas, its data structures ended up looking very complex, and it had the same \"fetch/navigate\" problem that we saw in the func attack example above.\n\nIt was simpler to just go straight to V2's reverse-walking approach.\n\nI'm also fairly certain V2's approach is faster, because it only has to process each node once in the phase 3 forward pass, and once in the phase 4 backward pass. [11](#note11)\n\nBut of course, the reverse-walk approach doesn't get loops for free. We still need some adjustments.\n\nThis bookkeeping might be decrementing a reference count, or [adding a destructor call](https://www.youtube.com/watch?v=IpuvQUVB8Cg), or checking for use-after-frees, etc.\n\nNote, in these examples, I use the name ArrayList, but in the real Valen compiler those are called Array, for weird historical reasons. That'll change soon.\n\nNote, this doesn't mean that the algorithm is O(n) yet. The MUTATE still needs to check everything in the pending list. However, I think there's a way to speed that check up a lot. Fingers crossed!\n\nV1 never had full loop support, so there's really nothing to benchmark against.\n\nThe algorithm needs a little bit more to handle loops.\n\nSpecifically, we'll do MUTATE and DECLARE differently, and we also react to LOOP START.\n\nWe scan upward, looking for these events:\n\nLet's see what this does to this example. It's a **correct** program, so there should be no compile error.\n\n``` js\nexported func main() {\n  let arr: ArrayList<int> = ...; // DECLARE arr (group arr)\n  let e = &arr[0];               // DECLARE e (group arr[])\n  \n  while ... {                    // LOOP START\n    observe(e);                  // USE e (group arr[])\n    add_element(&arr);           // MUTATE arr (group arr)\n    set e = &arr[0];             // DECLARE e (group arr[])\n  }\n}\n```\n\nWe're walking backwards, so we start at the last line:\n\n```\n    set e = &arr[0];             // DECLARE e (group arr[])\n```\n\nIt's a DECLARE [15](#note15) so we do two things:\n\nrevived_after_this now contains one entry: e.\n\nNow we walk up one line, seeing this:\n\n```\n    add_element(&arr);           // MUTATE arr (group arr)\n```\n\nIt's a MUTATE, so we do two things:\n\nmutations_after_this now contains one entry: arr.\n\n```\n    observe(e);                  // USE e (group arr[])\n```\n\nIt's a USE, so we **add** it to the pending list.\n\npending now contains one entry: e (group arr[]).\n\nNow we walk up one line, seeing the loop start:\n\n```\n  while ... {                    // LOOP START\n```\n\nSo we do the LOOP START check: For each thing in pending, if it's invalidated by something in the loop's mutations_after_this, ensure it's in the loop's revived_after_this.\n\nIn this case, pending has e (group arr[]). e *is* invalidated by something in mutations_after_this. So check it's in the loop's revived_after_this (it is). Check passed, we're good.\n\nI'll skip the explanation for the top two DECLAREs, since we covered that before.\n\nAnd then we reach the start of the function! We encountered no errors, so the borrow checker can conclude **this function is memory-safe.**\n\nAnd that's it! That's how we handle loops with this new reverse borrow checker.\n\nEaster egg note!\n\nSergeant Stubby was the star of the 102nd Infantry Regiment in World War I and travelled with his division to France to fight alongside the French. He served for 18 months and participated in 17 battles and four offensives on the Western Front.\n\nHe received many awards, including a gold medal, a wound stripe, and two purple hearts.\n\nSergeant Stubby has been called the most decorated war dog of the Great War and the only dog to be nominated and promoted to sergeant through combat.\n\nIf you see this note, sneak the word \"gold\" into a [comment](https://www.reddit.com/r/programming/comments/1x92rhh/a_new_way_to_think_about_borrow_checking_in/)!\n\n(Congrats to [timpi4ever](https://www.reddit.com/r/Valen/comments/1x92s1o/comment/phi1ieo/) for finding this first!)\n\nWe don't remove anything from pending here, because those pending uses still need to be satisfied by the code above the loop.\n\nYes, I regret calling it DECLARE now, since really this is just re-pointing an existing variable.\n\nThere's one more big thing I want to change, eventually.\n\nRust has this very nice ability to use facts from *anywhere* in the function to figure out the lifetime of reference.\n\nFor example, [this Rust program](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=70b5f84ea984e1a3a60dff9e0110bcf9) looks at the later r = &b[0]; to figure out that r points at a *or* b.\n\n``` js\nfn main() {\n  let a = vec![1, 2, 3];\n  let b = vec![4, 5, 6];\n\n  let mut r = &a[0];\n  if some_condition() {\n    r = &b[0];\n  }\n  println!(\"{}\", r);\n}\n```\n\nValen doesn't do this yet; Valen's phase 3 (static grouping) is still doing normal one-directional inference.\n\nBut if we change phase 3 into a full solve like rustc does, we can get some similar superpowers.\n\nThe neat part: this wouldn't need to change phase 4's backward pass. In the end, we'd have a solver for phase 3, and a backward pass for phase 4.\n\nIt's hard to explain how cool that is to me. It's moments like these I suspect that I might be a compiler nerd. [Huzzah!](https://www.youtube.com/watch?v=y13n1CeyqjY&t=132s)\n\nValen's borrow checker is still pretty young, a mere 3,600 lines.\n\nAnd it's far from complete! We still have to add a lot of things to it, like interrupting control flow (break, continue), [16](#note16) associated groups, closures, [17](#note17) all the fun stuff.\n\nWe'll get there one day. That day will be glorious.\n\nThanks for reading!\n\nIn the next few articles, I'm hoping to explain more about how we'll blend in reference counting and generational references. I'm also going to write about all the other languages exploring new memory safety approaches like this ([Ante](https://antelang.org/), [Carbon](https://github.com/carbon-language/carbon-lang), [Zeta](https://github.com/zeta-lang/zeta), [Fang](https://github.com/devast8a/Fang), [exclusion typing](https://dilated.me/blog/posts/introduction-to-exclusion-typing/), and more!).\n\nThis is just the tip of the iceberg, so stay tuned by subscribing to my [RSS feed](https://verdagon.dev/rss.xml), [r/valen](https://www.reddit.com/r/Valen/), or joining the [Valen discord](https://discord.gg/SNB8yGH). And follow me on [Bluesky](https://bsky.app/profile/verdagon.bsky.social) and [Mastodon](https://fosstodon.org/@verdagon)!\n\nCheers,\n\n- Evan Ovadia\n\nThe current plan is to first refactor things so the borrow checker is looking at a [control-flow graph](https://en.wikipedia.org/wiki/Control-flow_graph) instead of a typed AST. That should make interrupting control flow much easier.\n\nI'm pretty excited about closures. I think I found a *very* elegant way to make them work. Stay tuned!", "url": "https://wpnews.pro/news/a-new-way-to-think-about-borrow-checking-in-reverse", "canonical_source": "https://verdagon.dev/blog/reverse-borrow-checking", "published_at": "2026-10-18 12:11:13+00:00", "updated_at": "2026-10-19 00:19:33.012034+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Valen", "Rust", "LLVM"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/a-new-way-to-think-about-borrow-checking-in-reverse", "markdown": "https://wpnews.pro/news/a-new-way-to-think-about-borrow-checking-in-reverse.md", "text": "https://wpnews.pro/news/a-new-way-to-think-about-borrow-checking-in-reverse.txt", "jsonld": "https://wpnews.pro/news/a-new-way-to-think-about-borrow-checking-in-reverse.jsonld"}}