Rendered at 15:06:52 GMT+0000 (Coordinated Universal Time) with Cloudflare Workers.
zamalek 22 hours ago [-]
> Borrow checking has the "shared-xor-mutable" restriction: if you hold a reference to an object, nobody else can change the object.
The article makes this sound like a problem. It isn't. This restriction frees you from thinking about certain classes of bugs in concurrent code; it's where the "fearless concurrency" comes from. R^w isn't about lifetimes - you could, in spirit (not sure about practice), remove it from Rust without affecting use-after-free at all.
Of course, it means you can't write many completely valid programs, and so we'd hope there's a better solution, but removing it is not one.
If you want to have to think about that (and potentially introduce bugs), that's perfectly fine. The flexibility of not requiring r^w is extremely useful. I'm just clearing up this mis-attribution.
pron 18 hours ago [-]
The more you restrict the safe subset of the language, the more correct programs must use the unsafe subset. The logic of "there could be bugs here, therefore it's safer to restrict it" ends in an empty safe subset. The reason for spatial and (to a somewhat lesser degree) temporal memory safety even at the cost of excluding programs that don't violate them is that out-of-bounds access and UAF are extraordinarily overrepresented as causes of severe security vulnerabilities, but that's not the case for concurrency. There are more common causes of vulnerabilities that aren't caught by Rust's type system, that focusing on races seems arbitrary.
VorpalWay 7 hours ago [-]
Concurrency issues (such as torn writes) can easily cause out of bounds and similar UB on other threads, so focusing on races is not arbitrary if you want to be a systems level language.
Sure, you could pay the overhead of a managed language and have the runtime handle synchronization behind your back, but that isn't the path Rust went down.
pron 3 hours ago [-]
There are multiple things here (and I'm speaking as a systems prorgrammer of 25 years). First, you may want to look into the percentage of vulnerabilities caused by read/write races that lead to tearing and read/write races in general. Second, out-of-bounds on other threads as the result of tearing can and should be handled separately, and often are. Third, there are more common causes of security vulnerabilities and even of memory safety violations, including (though certainly not limited to) the need to use unsafe in safe situations because of the over-strictness of the type system. Rust does this because its design constraints require it, not because it's the right thing to do.
VorpalWay 1 hours ago [-]
Rust is specifically designed to ensure that safe code can never trigger UB (barring compiler bugs, or bugs in unsafe code blocks). For that, protecting against races is necessary (at least when using LLVM as a backend, and when using the C++ memory model).
In practice rust also tries to guide you towards patterns that reduces the risk of other bugs. For example, using Result, Option (and enums in general) reduces the risk that you forget to handle error paths. It is also quite hard to get memory leaks unintentionally in Rust, since by default you work with various smart pointers.
In my experience typical multi-threaded C++ has a lot of threading bugs that tend to cause hard to reproduce bugs (I work in safety critical hard realtime robotics / machine control, where we have a lot of communicating threads). When I code Rust I can trust the language to prevent those classes of bugs, letting me focus on the actual hard domain specific problems, rather than language minutiae. Yes, TSAN helps in C++, but that is dynamic analysis so it has to actually catch the potential problem happening while running.
So I think that the multithreading approach in Rust is really good and what we want, not a hindrance. It is absolutely the right thing to do.
Now, the async story is another matter and I have conflicting opinions on how that is done in rust. I can't think of a significantly better design that would work on microcontrollers / no-std though. I'm not interested in anything that requires allocations or a runtime.
creata 7 hours ago [-]
> but that's not the case for concurrency
Is that the case because it's easy to avoid concurrency issues, or because people are afraid of even trying to introduce concurrency into their program due to those issues? Because if it's the latter, then trying to support some amount of safe concurrency makes sense.
ngrilly 4 hours ago [-]
You're right, but what pron is saying is that it doesn't matter much from a security standpoint because they can't be exploited.
zamalek 8 hours ago [-]
It's always a trade off. I don't think that humans are capable of perfectly pulling off manual memory management or multi-threading, as a biological limitation of our wetware. But I may just be stupid.
Either way, the problems that rust takes off the plate don't seem arbitrary for that reason. I don't particularly mind the problems it adds to the plate.
Calling a trade-off a slippery slope to a zero-set language is really disingenuous.
pron 2 hours ago [-]
But that's not Rust's actual tradeoff. Safe Rust prevents data races not because they were separately considered as a tradeoff, but because its particular chosen design for memory safety requires it. It's a side-effect, not an orthogonal feature.
And I wasn't saying there's an actual slippery slope, only that the argument that if something is tricky it should be moved out of the safe subset is not a good one. The justification Rust uses to do this for memory safety is because memory safety was empirically found to be special, but that particular justification doesn't work for concurrency (nor is it the one used; again, in Rust this is a side-effect).
zozbot234 10 minutes ago [-]
> Safe Rust prevents data races not because they were separately considered as a tradeoff, but because its particular chosen design for memory safety requires it. It's a side-effect, not an orthogonal feature.
Data race freedom has been specified as a requirement for UB-freedom (i.e. safe code) since the C11/C++11 concurrency model, which predates the Rust borrow checker. You can make different choices on particular platforms, but they come with challenging tradeoffs: e.g. in Java, variables of type "long" are not data-race free on 32-bit platforms; writes to a long can silently tear.
verdagon 21 hours ago [-]
Just to clarify, Valen's borrow checker can express shared-xor-mutable, its value proposition is that it can _also_ express mutable aliasing (with `in`).
* When a function has a mutable effect into a group, and the function doesn't declare any other groups to alias it (with `in`), it is effectively a unique reference.
* When a function has references into a group, and it declares no `mut` effects into any of them, they are effectively shared/immutable references.
This helps with Valen's structured concurrency in particular, but also helps guard against the single-threaded race conditions in the same way that Rust's borrow checker does. It also helps with Rust interop!
So, TL;DR: Valen lets you choose between shared-xor-mutable and mutable aliasing.
Also note how mutable aliasing is opt-in (via `in`), so there's a subtle influence pushing people towards shared-xor-mutable, so that they only reach for mutable aliasing when it will benefit them.
zozbot234 19 hours ago [-]
> Just to clarify, Valen's borrow checker can express shared-xor-mutable, its value proposition is that it can _also_ express mutable aliasing (with `in`).
Just to clarify, Rust's borrow checker can express mutable aliasing via interior mutability (i.e. UnsafeCell<T> and the various Cell<T> types). A &Cell<T> reference is essentially a mutably aliased reference. The goal is exactly that people "only reach for mutable aliasing when it will benefit them". Of course, any ergonomic improvements around the Cell types are quite welcome, especially if they help C/C++ interop - provided that they're proven to be as sound as the existing borrowck.
verdagon 19 hours ago [-]
Good catch, and well said. Another good example I like citing is GhostCell, which adds mutable aliasing to Rust in a way that's even more flexible than Cell.
I would say Valen's real benefit here is in making a more ergonomic way for functions to work with an arbitrary number of GhostTokens / brands, and to track the relationships between them. (But I admit, I'm no expert with GhostCell, happy to be corrected by someone here)
zozbot234 19 hours ago [-]
AIUI, the main limitation with &Cell<T> is (1) no provision for concurrency, obviously - though the various Atomic* types work similarly via interior mutability; and (2) data has to be read, written or modified as a single operation - any provision for enabling subobject access or the like is quite ad hoc, and has to be implemented for the specific type you're working with. GhostCell (and QCell, LCell etc.) generalizes this by tracking the use of shared references at compile time, such that writes will not overlap with each other or with reads, without enforcing a "get/set in a single step" workflow.
Georgelemental 18 hours ago [-]
> any provision for enabling subobject access or the like is quite ad hoc, and has to be implemented for the specific type you're working with
There is ongoing work on a language feature ("field projection") that could alleviate this.
octoberfranklin 20 hours ago [-]
I think OP understands that.
And I think OP's point, more succinctly, is that Valen programs will be much more difficult to parallelize than Rust programs.
It's shockingly easy to make a single-threaded Rust program use all the cores on a machine by slapping in Rayon wherever you have a Vec. Because Rust forces you to do the hard work of proving shared^mutable before getting a single-threaded program running.
The ecosystem-wide consequence of this is that pretty much every compute-intensive program written in Rust (that doesn't rely on non-Rust libraries for compute-intensive stuff) is automatically multicore. This is one of the reasons why Rust programmers seek out Rust libraries first. Because they know they won't get the unpleasant surprise of putting in a lot of work to adopt a library and then get burned when they find out it will only use a single core.
verdagon 19 hours ago [-]
Apologies for not understanding. And I admit, it's hard to communicate in the abstract, so I might still not understand the question.
If it helps: AFAICT, Valen's borrow checker preserves the same ecosystem-wide concurrency benefits that Rust has. For precedent, check out GhostCell [0] which is not only _compatible_ with Rust's concurrency but gives it some interesting new abilities. Valen's approach could be thought of as a more ergonomic form of GhostCell that better tracks the relationships between multiple groups (brands).
I could give a better answer if we had an example to toss around where we think Valen might force things to be single-threaded.
zozbot234 19 hours ago [-]
Rust needs shared xor mutable in order to prevent data races on non-atomically accessed data. You said that Valen can _express_ shared xor mutable, but perhaps you haven't clarified whether this also interacts with equivalents to Rust's Send and Sync to enable the same fearless concurrency.
torginus 19 hours ago [-]
I mean the article doesn't talk about this aspect of the language, but Send and Sync are orthogonal to borrow checking. I don't see why you couldn't use Rust's exact design in Valen.
uhhuak 20 hours ago [-]
How widespread are locks in typical Rust code?
octoberfranklin 17 hours ago [-]
Very rare compared to C code.
dgrunwald 5 hours ago [-]
If a language allows taking references to fields within a tagged union (as is possible with Rust enums), then "shared-xor-mutable" is required to avoid use-after-free (switching the tagged union to another variant ends the lifetime of the old variant's fields).
uhhuak 20 hours ago [-]
> "fearless concurrency"
That saying does more harm than good, and it counts against the Rust community that it keeps repeating it. Deadlocks are not something that Rust prevents. On the contrary, many beginners to Rust often run into deadlocks, some even spurred on by trying to satisfy the borrow checker. An infamous example is https://fasterthanli.me/articles/a-rust-match-made-in-hell . Instead of making blatantly false claims and causing newcomers to Rust frustration, pain and bugs, the Rust community should warn about concurrency and direct beginners to learn about concurrency, both generally as well as specifically in Rust.
creata 20 hours ago [-]
* Deadlocks, memory leaks, infinite loops, etc. are all the sort of risks that Rust doesn't try to protect from. In my opinion it's okay to talk about "fearless concurrency" despite deadlocks in the same way it's okay to call Rust memory-safe despite memory leaks.
* Rust's type system also allows you to make safe interfaces like Rayon that do genuinely allow for "fearless" concurrency in a way that I don't really see in other general-purpose languages.
uhhuak 20 hours ago [-]
[flagged]
creata 20 hours ago [-]
I'm not part of "the Rust community". I'm not sure why you have to be so inflammatory.
Those are bullet points.
kvark 24 hours ago [-]
It sounds more like "path" borrowing than "group" borrowing to me, but the idea is great! Quite eye opening!
One minor thing is that I'm not sure why they had to have "in" keyword for sub-borrows. Wouldn't it be more consistent to see "entity &world.entities[?]" instead of "entity in world.entities[]"? We'd consistently get the borrow "&" symbol and open the door for some contracts on what index (range) is affected.
cmrx64 22 hours ago [-]
(hi kvark, long time!) it’s a clever algorithm, if you’re curious about other factorizations of referentiality frames I’ve been tinkering with analyzer across interpretation boundary like the gpu command queue :) I need to get some writing together. the techniques like from this post are a little “cute” compared to what real systems need.
verdagon 1 days ago [-]
Also, as I was writing this, a couple things occurred to me:
* We _could_ use the function parameter syntax `entities: &world.entities[]` instead of `entities in world.entities[]`.
* "Groups" aren't really central to understanding the idea, so Path Borrowing might be a better name than Group Borrowing.
Opinions welcome =)
munchler 1 days ago [-]
FWIW `foo in bar` implies iteration to me, not a path. In the .NET world, for example, `bar` would be a collection that we’re iterating through, assigning each element to `foo` in turn.
torginus 7 hours ago [-]
I had the idea to think about path checking as compile-time tracing GC. You have GC roots know by the compiler, and at any point in the code, you can perform liveness analysis, with roots on the stack, and every time a root goes out of scope, you do 'liveness analyisis' and deallocate everything that went out of scope.
_old_dude_ 22 hours ago [-]
Assuming “groups” are mainly about grouped invalidation, I agree, Path Borrowing
is the better name, since it describes the core idea rather than the implementation mechanism.
falserum 19 hours ago [-]
A bit more fuel for the bikesheding aspect of this:
Subtree borrowing.
(Single ownership stemming from main is a tree of owners; each path specifies some subtree)
(Btw. I enjoyed reading it)
scotty79 23 hours ago [-]
Love both.
dualvariable 16 hours ago [-]
> It's part of my eternal quest to find the memory safety holy grail: something as powerful as Rust's borrow checking, yet something as simple and flexible as reference counting or garbage collection.
I'm definitely hoping you succeed. I'd love to be able to write a small language (maybe that didn't do absolutely every last thing you can do in Rust, but was ergonomic and easy to use) that had a borrow checker and didn't require writing a nuclear reactor.
pjmlp 5 hours ago [-]
There are more powerful algorithms than what Rust does, starting by how Cyclone did it, and the use of dependent types, proofs, effects and capacities in functional languages.
Granted, many people only know that from Rust, so they think of it as some kind of revelation.
Panzerschrek 9 hours ago [-]
Try my language: https://github.com/Panzerschrek/U-00DC-Sprache/.
It's memory safe (as Rust), but is way simpler (no explicit borrowing and traits hell) and it's already pretty developed (not in an experimental stage).
giovannibonetti 1 days ago [-]
I wonder if nowadays we should be focusing less on scalar data structures and more on languages that facilitate vectorized/SIMD instructions. Languages like Vx lang, Mojo, and Futhark that work both in the CPU or the GPU, although each one works in a different level of abstraction and control.
verdagon 1 days ago [-]
GPU compilation is definitely something I intend Valen to support. I wrote about it back in 2022, [0] and I learned a lot of lessons on how to do it (and how not to do it!) from working on the Mojo compiler.
The biggest decision for Valen is what to lower to:
* Rust MIR, since rustc has CUDA now, [1] (perhaps other cards soon?)
* SPIR-V, like Zig does for its GPU compilation [2]
* MLIR
Rust MIR is looking pretty nice. Valen already has Rust interop by doing some rustc sorcery, [3] and it would be somewhat straightforward to switch from emitting LLVM to emitting Rust MIR. I just need to figure out if Rust MIR can support the optimizations I have planned for Valen.
In a perfect world, Rust would be able to lower its MIR to MLIR, since MLIR has so many backends. I recall there were some efforts to do that, unsure where that ended up.
I don’t want to hijack Evan’s post by posting a link, but if you’re interested I’ve implemented many of the ideas in that presentation in the language I’m working on. You can check out the latest link in my post history for a rundown of how it works and a demo.
verdagon 1 days ago [-]
Feel free to talk about it here, would love to hear about it!
cmontella 16 hours ago [-]
I'll submit a post to HN soon, probably not on r/programminglanguages since I'm using more AI than they'd appreciate in my work these days.
mshockwave 18 hours ago [-]
is it the mech-lang?
cmontella 16 hours ago [-]
Yes!
amluto 23 hours ago [-]
Neat!
I have a question about immutability. In Rust, if I have a shared reference to T (an &T a variable or a parameter), then I have a restriction that I can't modify T or anything in it (which Valen thinks is annoyingly restrictive, and I tend to agree), but I also have a promise that no one else will modify it. The latter is quite nice: it makes the optimizer happier (improves aliasing analysis), makes threading happier (nothing descended from the reference can have data races while the reference is alive), and makes me happier (I don't need to think about descendent values being mutated).
Valen can call into Rust, and I think I can see how, at the site of any particular call, Valen can tell that no one is mutating the referent or its descendents: in a single-threaded world, the only thing executing is the current line of code or a maybe a few consecutive lines of code, and the compiler can see the function's signature and any mutable references therein, and if there is no permission to modify a descendent, then it doesn't get modified.
But in a multithreaded world, especially if calling into Rust in a thread, doesn't there need to be a way to guarantee the immutability of an object across an entire region of code? How does that work in Valen?
And for making immutability more comprehensible to people and to local analysis in general, would a special type of reference meaning "yes, this one really is fully frozen and there are no mutable paths into it for the entire lifetime of this reference" be a nice feature?
(Aside: I've occasionally contemplated whether Rust would benefit from another flavor of reference: no-access. A no-access reference would guarantee the referent's existence but could coexist with shared and with mutable references. Safe code would be unable to read or write through such a reference. Other than making some cell-like types mildly less mind-bending, I'm not convinced I have an actual justification for this thing. This would give Rust three flavors of references.
But I can imagine a Valen-like language having three flavors of references: frozen references (cannot use them to mutate and there's a promise that no one else can either), exclusive references (fully mutable, etc, just like Rust's &mut) and flexible references (the kind of reference in the blog post).)
verdagon 21 hours ago [-]
Awesome question, you're getting at the good stuff.
Short answer: Valen would have something similar to Fn and FnMut (but phrased in terms of effects rather than Fn vs FnMut). In other words, we would be able to express "a closure that does not modify anything it captures", or rather, "a closure that has no mut effects".
That closure, because it doesn't modify anything it captures, would be safe to share among multiple threads in a structured-concurrency-like / std::thread::scope-ish way.
The key here is that one _can_ express immutable references in Valen; an immutable reference is a reference that the containing function doesn't express a `mut` effect for. And once we have immutable references, we get all of the nice concurrency benefits that Rust trailblazed.
I'd also like to make a way to do the above without a function call, perhaps using something like the `parallel` keyword I described in [0].
A no-access reference is an interesting idea. That could be a more powerful way to express may_dangle. In Valen, I hope to have an "opaque" group to express something like that.
I don't know whether it's a good idea, but in Valen I'm trying to decouple access capabilities away from the reference types as much as possible. We'll see if that bet pays off.
> The key here is that one _can_ express immutable references in Valen; an immutable reference is a reference that the containing function doesn't express a `mut` effect for. And once we have immutable references, we get all of the nice concurrency benefits that Rust trailblazed.
I'm contemplating this. Is it enough?
Suppose I have an object (I'm not even trying to get the syntax right, especially since Valen's syntax appears a bit different from Rust's):
let obj: T = ...;
And I also create a structured concurrency thingy in the same scope:
let workgroup: StructuredConcurrencyThingy = ...;
Now I pass references to both of these down the callchain, through a few functions, maybe via some structs with lifetime parameters, and in the inner function I do this:
workgroup.submit(move || print_in_rust(obj));
where print_in_rust is a Rust function taking &T. (I haven't the faintest clue how to spell that in Valen.) So I'm making a closure, and the closure captures obj, and the closure needs obj to exist and be immutable for the lifetime of the closure, which exceeds the creating function's lifetime. It's bounded by the workgroup's lifetime, and Rust is fine with this.
But, if I'm understanding you right, the immutability of the referent of obj depends on the signatures of everything in the callchain that might execute during the lifetime of the closure. How does that work?
edit: On further contemplation, I don't think that actual concurrency is needed to illustrate it. I think the same issue exists if I have a T<'a> that has a method that takes an &'a reference (probably like store_a_reference(&mut self, ref: &'a u32)) and dereferences ref both immediately and later and asserts that it sees the same value both times.
verdagon 19 hours ago [-]
Sorry, I think my attempts to simplify/explain ended up confusing things. I'll try to be a little more precise.
In Rust, a reference is forever shared/immutable or forever unique/mutable.
In Valen, a reference is... "it depends". Specifically, it depends on the context.
It's similar to a &GhostCell<T>, where its mutability isn't determined yet because the GhostToken isn't present yet.
So, what determines the mutability at any given point in time? The function's `mut` effects (or lack of them) for the group/path that the reference is pointing to.
So we can imagine a `execute_on_4_threads` function like this:
(`Func<void, (), C>` is a trait for a function that returns void, takes no extra parameters, and names its captures as group C).
The most relevant fact here is that this function doesn't declare any `mut` effects at all (not on C, not on W's group, nothing), so nothing is being mutated. (And because of that, this function's callees also can't have any `mut` effects; they also can't mutate the data)
Now let's say we changed `workgroup`'s type to `&W in w`, and added a `mut(w)` to the function, to describe that we might modify the workgroup.
At that point, we would still know that we can invoke the closure from 4 threads, because we declared no relationship between `w` and `C`, so the compiler assumes (and enforces) that they're disjoint, have no overlap, nothing in one aliases anything in the other.
Hopefully that helps. Maybe I should write a blog post on this, my posts are usually clearer than my HN comments.
(Also, I'm not sure I understand your edit, if you could clarify that would be much appreciated)
amluto 17 hours ago [-]
I think I'm still failing to adequately explain what I mean. Let me try being absurdly explicit. The following is ordinary Rust code, and the reader is supposed to pretend that the two functions prefixed with valen_ are in Valen instead.
#[derive(Default)]
struct Receiver<'a> {
ref_and_val: Option<(&'a u32, u32)>
}
impl<'a> Receiver<'a> {
fn set(&mut self, reference: &'a u32) -> ()
{
self.ref_and_val = Some((reference, *reference))
}
fn check(&self) -> ()
{
if let Some((reference, val)) = self.ref_and_val {
if *reference != val {
panic!("The impossible happened!")
}
}
}
}
fn valen_inner_fun<'a>(r: &mut Receiver<'a>, val: &'a u32)
{
r.set(val)
}
fn valen_intermediate_fun<'a>(r: &mut Receiver<'a>, val: &'a mut u32)
{
// In a real mixed-language example, this would be in Valen, not Rust,
// and it would declare a mutable effect on val.
valen_inner_fun(r, val);
// And it would do this, which Rust disallows.
*val = 17
}
fn main() {
let mut r: Receiver = Receiver::default();
let mut fortytwo = 42;
valen_intermediate_fun(&mut r, &mut fortytwo);
r.check()
}
This compiles except for the *val = 17 line. (rustc's error message is a bit confused, and maybe I'll file a bug about that. If one follow's rustc's advice, the weird error turns into a less weird error.)
My point is that Receiver::set() requires a promise that its parameter's referent is immutable for the entire lifetime 'a, which exceeds the lexical duration of set() itself. And valen_outer_fun in Rust knows this to be true as a result of Rust's shared-xor-mutable system, and Rust's borrow checker verifies this:
a) the code would compile if I removed the offending *val = 17
b) the code, correctly, does not compile as written
c) If I force the issue by replacing *val = 17 with:
unsafe { *(val as *const u32 as *mut u32) = 17 }
then it panics and we can all imagine we're using C++ instead of Rust :)
But valen_inner_fun does not mutate val, and if I understand right then this means that Valen would accept its Valen equivalent without mut(val), and this seems unsound to me.
When I first thought of this, I imagined set() instead being a work-dispatching function that would run its passed-in closure and hit UB due to a data race with the *val = 17 line, and my edit was my realizing that there's a simpler non-concurrent example.
AlotOfReading 21 hours ago [-]
I've occasionally contemplated whether Rust would benefit from another flavor of reference: no-access.
Most of the uses for this I can think of are best solved by opaque pointers for FFI. Having a rust-native reference just seems incongruous. Like, does it have size and alignment info? How would it interact with NLL? The only way I can imagine it working is if it extended referent lifetime throughout the lexical lifetime of the reference, but that defeats the purpose of NLL.
Rust solves a similar problem in closures with unique immutable references, but they're not quite the same.
amluto 20 hours ago [-]
In my mind, a no-access reference would have exactly the same lifetime rules (except for the exclusivity part) as any other reference. And they'd have the same size and lifetime rules.
Unsafe Rust could promote a no-access reference to a shared or a mutable reference, and the unsafe code would be responsible for not violating exclusivity rules but would have a guarantee that the referent actually exists. Using unsafe code to create a no-access reference to a nonexistent object or to a misaligned object would be UB.
Safe code could convert the other way:
let a: &mut u32 = ...;
let b: &noaccess u32 = a;
let c: &u32 = ...;
let d: &noaccess u32 = c;
This is not an entirely serious proposal.
AlotOfReading 19 hours ago [-]
Under NLL, reference lifetime is defined by the places where the reference is used, and noaccess types can't be used. So then reference lifetime has to devolve to the spans where the reference is live, which is only defined lexically.
So for example:
let mut data = vec!['a', 'b', 'c'];
let b: &noaccess [char] = &data[..];
data.push('d'); // Does this error?
Hence the question
amluto 19 hours ago [-]
Aha, gotcha. I assume that any such reference would either do nothing or would be used by unsafe code (presumably by conversion through a raw pointer but maybe direct unsafe conversion to regular references could also be allowed).
Your example is sneaky, though. Lifetime issues aside (suppose the next line of code uses b), that’s a noaccess reference to memory (an object? a place? I’m not sure what the current term is) that is only guaranteed to exist so long as data is not mutated. So the code with a subsequent use of data would error.
But if it were instead:
let b: &noaccess = data;
Then it would not error.
zozbot234 19 hours ago [-]
It may be interesting to focus on how exactly these planned "no-access" references would differ from raw pointers? Access through raw pointers is an unsafe operation already.
peesem 1 days ago [-]
nit: you need a better way of laying out asides/footnotes. when they get bunched up like at the start of this article you start having to scroll full screens back and forth. at least make the numbers on the asides link back to their position in the main text
Cpoll 24 hours ago [-]
They're fragment links, so you can use your browser's back button to go back to where you were.
verdagon 1 days ago [-]
Yes, absolutely, lol. This article is really pushing my notes system past its limits. And it loops between six note colors, which normally isn't a problem, but is here. During the holidays, I want to see if I can implement Tufte notes like in https://edwardtufte.github.io/tufte-css/
Hunpeter 22 hours ago [-]
Off-topic (as I don't have the knowledge to intelligently comment on the concept): all I can think when I hear the name "Valen" is Babylon 5. I wonder if it's a coincidence or a deliberate reference?
verdagon 18 hours ago [-]
Perhaps Valen will unite borrowing, reference counting, and generational references... just like how a certain Babylon 5 figure united the fractured Minbari Castes and lead them to victory.
variaga 22 hours ago [-]
The about page says Valen is a successor to the Vale programming language, so I doubt it.
Panzerschrek 9 hours ago [-]
> the compiler remembers where a reference points to
> So we spent months sharpening the explanation and figuring out its benefits, and wrote a post 8 on it last year, hoping other languages would pick it up.
My language (see my submissions) has this feature since many years. In its current form it exists roughly since year 2020, but some under-the-hood changes and slight improvements have been done since.
Animats 17 hours ago [-]
> But group borrowing actually resolves the conflict, by relaxing "shared-xor-mutable" to "no use-after-free".
That's the one line that defines what's going on here.
If all you want is "no use after free", either reference counts or garbage collection will do the job. That's how most GC-oriented languages work.
Rust enforces stricter rules - single ownership, one writer or N readers. The question is whether that's worth the trouble. In threaded programs, it definitely is, because it prevents race conditions. Whether it's a win for single-thread programs is a good question.
Vale seems to be a way to move much of the overhead of a reference-counted system at compile time. Nothing wrong with that. The main motivation seems to be better interoperability with less restrictive languages. Am I reading that correctly?
gpm 17 hours ago [-]
I don't think you're reading it correctly.
I'd describe this as approximately weakening aliasing xor mutability to interior-aliasing xor mutability. The motivation seeming to be to have a more expressive system that rejects fewer correct programs. With a secondary goal of interacting with more (not less) restrictive languages (namely rust).
sebastianmestre 1 days ago [-]
IIRC this Verdagon guy had a language called Vale... is Valen a rename or a new project?
Edit: tfa makes it clear it's a separate project
verdagon 1 days ago [-]
Hi! Kind of both. Even though Valen reuses 90% of the Vale compiler, its approach (Rust interop, borrow checking with mutable aliasing, etc.) is so different that it really needed a new name.
Also, I really like where Vale ended up, Vale's generational references + region borrowing was a truly weird and unusual memory safety blend. I didn't want that combination to be lost to time, so I wanted "Vale" to keep referring to that.
Arodex 19 hours ago [-]
A bit sad to see Vale archived. Have you written a final post on it (lessons learned, the good and the bad, fundamental strengths and weaknesses, future research needed, all that)?
verdagon 18 hours ago [-]
No, but that's a great idea. Vale definitely taught me a lot about languages and architecture. I might do that...
Arodex 18 hours ago [-]
Thank you. Your writings on memory safety and other similar topics is appreciated, and the work you are doing seems to point in a very interesting direction that I am sure will bring a new breakthrough similar to what Rust (or, more accurately, Cyclone) brought. Very interested in what will percolate in Mojo and Ante too.
melodyogonna 22 hours ago [-]
Mojo's origin can represent a lot of these semantics. I think a lot was learned from Nick's proposal, even if it wasn't directly implemented. Here is an example of how you could represent the first snippet, where two references can update the same list: https://godbolt.org/z/78bhzWjYM
verdagon 21 hours ago [-]
Unfortunately, Mojo kind of limits itself by forcing all parameters to be unique references or shared references. So the first example works in Mojo, but not the `step` example nor the `attack` example from the original group borrowing explanation. [0]
I hope they one day upgrade to full group borrowing, it would be a nice fit for them.
The ability to hold a reference to a container and its member seems to be interesting. But I don't think it adds significant value in practice (behind some synthesized examples) and doesn't worth to increase language complexity language compared for a more coarse reference analysis (variable-based).
EliasLittle 19 hours ago [-]
This is super exciting!
Having mutability not be a property of the data, but of the function arguments reminds me a lot of modes from Jane Street’s OxCaml ^1 which is really interesting to me, as OxCaml’s focus is not really about memory management (they still use garbage collection for everything not on the stack). Feels like we might be converging towards a new standard! I can see the morning sun on the horizon :)
> Having mutability not be a property of the data, but of the function arguments [...]
I don't know if you were aware of this, but Rust has that too. (Actually it's a property of any binding, not just function parameters)
EliasLittle 18 hours ago [-]
Interesting! I can’t say I’m that familiar with Rust beyond surface level usage. What feels different is that you can declare a reference as mutable or not in Rust whereas in Valen or OxCaml it’s strictly declared in a function’s arguments.
maufl 1 days ago [-]
How likely is it that such a borrow checker could be "backported" to Rust? Maybe in a new edition?
verdagon 1 days ago [-]
You know, it's not crazy. Niko wrote about lifetimes based on places back in 2024, [0] though it was in the context of shared-xor-mutable, which is consistent with Rust's spirit.
And Rust already supports forms of mutable aliasing already, such as with Cell, and GhostCell which is halfway towards Valen's approach. I would love to see someone augment GhostCell to have the sort of "invalidation" logic that group borrowing does. In fact, Plecra is working on something like that with their (WIP) "Exclusion Typing". [1]
On top of that, Polonius already thinks in terms of "paths", just like Valen does. So it's not so much a question of compiler design, but of language design, and how Rust would expose it to the user.
It would have to take &Cell<T> references or the like in order to preserve the existing uniqueness properties for &mut T references. Not so different ultimately from what GhostCell, QCell, LCell etc. do already, except possibly simpler in some ways.
user142 1 days ago [-]
I noticed that there is no control flow in the examples.
verdagon 1 days ago [-]
Next article =) How the borrow checker handles ifs and loops is a pretty fun topic. If-statements work like you'd expect (invalidations from both branches are merged). Loops is where it gets weird: we have to scout all the invalidations that happen inside the loop and then "replay" them _before_ the body of the loop. I can explain it more if you'd like.
user142 1 days ago [-]
I was wondering about the case where a reference points to a different path depending on control flow. I guess you can just design your language to make this impossible but this is still possible to do with pointers in C/C++.
verdagon 1 days ago [-]
Theoretically, we can have a reference that points to two paths, a "path union" so to speak. If the user explicitly typed out the path union, it would be something like:
let my_ref &Entity in (live_list[], dead_list[]) = if ...
though it would be nicer in practice, because the compiler could infer that.
Haven't implemented it yet of course, but the data structures in the borrow checker are designed with this in mind.
eptcyka 23 hours ago [-]
Would it then be possible to mutably reference two entries in the same hashmap?
verdagon 21 hours ago [-]
Yep, that comes for free from the model. Though, if you're asking if we can have two unique references (like `&mut`) to two different entries, not yet. Nick's original proposal had some thoughts on how we can do that, and Zeta (another language implementing group borrowing) has some neat dependent-type-ish mechanisms for that. I'm still considering what Valen will want to do there.
This looks cool for code-driven systems where code has static knowledge of every path in the system. It might be strictly better for things like rendering where types of rendering are code-driven. Like your world can have skybox and such. The examples assuming gamedev 'world' and 'entity' are completely misleading though. Because I think everyone has moved on to data-driven worlds. Think of entity 'advance' from the code examples as an 'entity blueprint execute'.
Checking the following code, could you confirm that is does not work concurrently ?
The "world" var is read-only, does it mean that all other entities are also readonly (cannot be modified by another thread, for instance) ?
struct World {
entities Vec<Entity>;
}
func step(world &World, entity in world.entities[] mut) {
entity.advance();
let collision = world.get_collision_for_entity(entity);
entity.resolve(collision);
}
verdagon 21 hours ago [-]
Yep, that works today. The neat thing here is that `world` is immutable _except_ for its `.entities[]` elements.
That wouldn't be shareable with another thread, because part of it is `mut`. _Theoretically_ we could make this shareable with other threads if we had another kind of effect, let me know if you're curious about that.
The `entity in world.entities[] mut` gives `step` blanket permission to mutate any element inside world.entities. We wouldn't be able to say "all other entities are readonly".
Though, it's worth mentioning that `advance` and `resolve` both receive the entity as a unique reference (since it's the only reference into a group which has a mut effect).
fithisux 1 days ago [-]
Is it possible to retrofit it to Freepascal, D, Freebasic or even Java?
verdagon 1 days ago [-]
Maybe! I think about D a lot when designing Valen, since they attempted to blend borrowing with garbage collection. Swift is facing some of the same challenges, and I expect Java could soon too now that Valhalla has landed.
If Valen solves those challenges as well as I think it can, it could be a good direction for them to explore.
fithisux 20 hours ago [-]
D is my favorite (I am a dinosaur) and I hope both Valen and D progress on that front.
octoberfranklin 20 hours ago [-]
A function signature describes the paths it modifies.
The problem with this is that it imposes a cost on abstraction. Zero-cost abstraction is one of the most fundamental design principles behind both C++ and Rust.
A field-path into a struct/enum fundamentally depends on its concrete implementation. If you hide the fields of a struct and use getters/setters, you break the ability to talk about the "paths modified" by a function which uses getters/setters.
Aside: it really annoyed me that I had to read halfway through this article to find the first attempt at defining "group borrowing". The whole first half of the article is basically fluff.
swiftcoder 20 hours ago [-]
> The whole first half of the article is basically fluff
There are multiple links in the article that suggest the reader skip to the good stuff if they are already familiar with how borrow checking works
20 hours ago [-]
falconBrisk47 1 days ago [-]
Path Borrowing reads clearer to me, "group" made me look for a group type.
The article makes this sound like a problem. It isn't. This restriction frees you from thinking about certain classes of bugs in concurrent code; it's where the "fearless concurrency" comes from. R^w isn't about lifetimes - you could, in spirit (not sure about practice), remove it from Rust without affecting use-after-free at all.
Of course, it means you can't write many completely valid programs, and so we'd hope there's a better solution, but removing it is not one.
If you want to have to think about that (and potentially introduce bugs), that's perfectly fine. The flexibility of not requiring r^w is extremely useful. I'm just clearing up this mis-attribution.
Sure, you could pay the overhead of a managed language and have the runtime handle synchronization behind your back, but that isn't the path Rust went down.
In practice rust also tries to guide you towards patterns that reduces the risk of other bugs. For example, using Result, Option (and enums in general) reduces the risk that you forget to handle error paths. It is also quite hard to get memory leaks unintentionally in Rust, since by default you work with various smart pointers.
In my experience typical multi-threaded C++ has a lot of threading bugs that tend to cause hard to reproduce bugs (I work in safety critical hard realtime robotics / machine control, where we have a lot of communicating threads). When I code Rust I can trust the language to prevent those classes of bugs, letting me focus on the actual hard domain specific problems, rather than language minutiae. Yes, TSAN helps in C++, but that is dynamic analysis so it has to actually catch the potential problem happening while running.
So I think that the multithreading approach in Rust is really good and what we want, not a hindrance. It is absolutely the right thing to do.
Now, the async story is another matter and I have conflicting opinions on how that is done in rust. I can't think of a significantly better design that would work on microcontrollers / no-std though. I'm not interested in anything that requires allocations or a runtime.
Is that the case because it's easy to avoid concurrency issues, or because people are afraid of even trying to introduce concurrency into their program due to those issues? Because if it's the latter, then trying to support some amount of safe concurrency makes sense.
Either way, the problems that rust takes off the plate don't seem arbitrary for that reason. I don't particularly mind the problems it adds to the plate.
Calling a trade-off a slippery slope to a zero-set language is really disingenuous.
And I wasn't saying there's an actual slippery slope, only that the argument that if something is tricky it should be moved out of the safe subset is not a good one. The justification Rust uses to do this for memory safety is because memory safety was empirically found to be special, but that particular justification doesn't work for concurrency (nor is it the one used; again, in Rust this is a side-effect).
Data race freedom has been specified as a requirement for UB-freedom (i.e. safe code) since the C11/C++11 concurrency model, which predates the Rust borrow checker. You can make different choices on particular platforms, but they come with challenging tradeoffs: e.g. in Java, variables of type "long" are not data-race free on 32-bit platforms; writes to a long can silently tear.
* When a function has a mutable effect into a group, and the function doesn't declare any other groups to alias it (with `in`), it is effectively a unique reference.
* When a function has references into a group, and it declares no `mut` effects into any of them, they are effectively shared/immutable references.
This helps with Valen's structured concurrency in particular, but also helps guard against the single-threaded race conditions in the same way that Rust's borrow checker does. It also helps with Rust interop!
So, TL;DR: Valen lets you choose between shared-xor-mutable and mutable aliasing.
Also note how mutable aliasing is opt-in (via `in`), so there's a subtle influence pushing people towards shared-xor-mutable, so that they only reach for mutable aliasing when it will benefit them.
Just to clarify, Rust's borrow checker can express mutable aliasing via interior mutability (i.e. UnsafeCell<T> and the various Cell<T> types). A &Cell<T> reference is essentially a mutably aliased reference. The goal is exactly that people "only reach for mutable aliasing when it will benefit them". Of course, any ergonomic improvements around the Cell types are quite welcome, especially if they help C/C++ interop - provided that they're proven to be as sound as the existing borrowck.
I would say Valen's real benefit here is in making a more ergonomic way for functions to work with an arbitrary number of GhostTokens / brands, and to track the relationships between them. (But I admit, I'm no expert with GhostCell, happy to be corrected by someone here)
There is ongoing work on a language feature ("field projection") that could alleviate this.
And I think OP's point, more succinctly, is that Valen programs will be much more difficult to parallelize than Rust programs.
It's shockingly easy to make a single-threaded Rust program use all the cores on a machine by slapping in Rayon wherever you have a Vec. Because Rust forces you to do the hard work of proving shared^mutable before getting a single-threaded program running.
The ecosystem-wide consequence of this is that pretty much every compute-intensive program written in Rust (that doesn't rely on non-Rust libraries for compute-intensive stuff) is automatically multicore. This is one of the reasons why Rust programmers seek out Rust libraries first. Because they know they won't get the unpleasant surprise of putting in a lot of work to adopt a library and then get burned when they find out it will only use a single core.
If it helps: AFAICT, Valen's borrow checker preserves the same ecosystem-wide concurrency benefits that Rust has. For precedent, check out GhostCell [0] which is not only _compatible_ with Rust's concurrency but gives it some interesting new abilities. Valen's approach could be thought of as a more ergonomic form of GhostCell that better tracks the relationships between multiple groups (brands).
I could give a better answer if we had an example to toss around where we think Valen might force things to be single-threaded.
That saying does more harm than good, and it counts against the Rust community that it keeps repeating it. Deadlocks are not something that Rust prevents. On the contrary, many beginners to Rust often run into deadlocks, some even spurred on by trying to satisfy the borrow checker. An infamous example is https://fasterthanli.me/articles/a-rust-match-made-in-hell . Instead of making blatantly false claims and causing newcomers to Rust frustration, pain and bugs, the Rust community should warn about concurrency and direct beginners to learn about concurrency, both generally as well as specifically in Rust.
* Rust's type system also allows you to make safe interfaces like Rayon that do genuinely allow for "fearless" concurrency in a way that I don't really see in other general-purpose languages.
Those are bullet points.
One minor thing is that I'm not sure why they had to have "in" keyword for sub-borrows. Wouldn't it be more consistent to see "entity &world.entities[?]" instead of "entity in world.entities[]"? We'd consistently get the borrow "&" symbol and open the door for some contracts on what index (range) is affected.
* We _could_ use the function parameter syntax `entities: &world.entities[]` instead of `entities in world.entities[]`.
* "Groups" aren't really central to understanding the idea, so Path Borrowing might be a better name than Group Borrowing.
Opinions welcome =)
Subtree borrowing.
(Single ownership stemming from main is a tree of owners; each path specifies some subtree)
(Btw. I enjoyed reading it)
I'm definitely hoping you succeed. I'd love to be able to write a small language (maybe that didn't do absolutely every last thing you can do in Rust, but was ergonomic and easy to use) that had a borrow checker and didn't require writing a nuclear reactor.
Granted, many people only know that from Rust, so they think of it as some kind of revelation.
The biggest decision for Valen is what to lower to:
* Rust MIR, since rustc has CUDA now, [1] (perhaps other cards soon?)
* SPIR-V, like Zig does for its GPU compilation [2]
* MLIR
Rust MIR is looking pretty nice. Valen already has Rust interop by doing some rustc sorcery, [3] and it would be somewhat straightforward to switch from emitting LLVM to emitting Rust MIR. I just need to figure out if Rust MIR can support the optimizations I have planned for Valen.
In a perfect world, Rust would be able to lower its MIR to MLIR, since MLIR has so many backends. I recall there were some efforts to do that, unsure where that ended up.
[0] https://verdagon.dev/blog/next-gen-languages-gpu
[1] https://developer.nvidia.com/blog/introducing-cuda-rust-two-...
[2] https://ziglang.org/devlog/2026/#2026-06-26
[3] https://verdagon.dev/blog/golden-spike-reviving-vale-valen
https://venge.net/graydon/talks/VectorizedInterpretersTalk-2...
I have a question about immutability. In Rust, if I have a shared reference to T (an &T a variable or a parameter), then I have a restriction that I can't modify T or anything in it (which Valen thinks is annoyingly restrictive, and I tend to agree), but I also have a promise that no one else will modify it. The latter is quite nice: it makes the optimizer happier (improves aliasing analysis), makes threading happier (nothing descended from the reference can have data races while the reference is alive), and makes me happier (I don't need to think about descendent values being mutated).
Valen can call into Rust, and I think I can see how, at the site of any particular call, Valen can tell that no one is mutating the referent or its descendents: in a single-threaded world, the only thing executing is the current line of code or a maybe a few consecutive lines of code, and the compiler can see the function's signature and any mutable references therein, and if there is no permission to modify a descendent, then it doesn't get modified.
But in a multithreaded world, especially if calling into Rust in a thread, doesn't there need to be a way to guarantee the immutability of an object across an entire region of code? How does that work in Valen?
And for making immutability more comprehensible to people and to local analysis in general, would a special type of reference meaning "yes, this one really is fully frozen and there are no mutable paths into it for the entire lifetime of this reference" be a nice feature?
(Aside: I've occasionally contemplated whether Rust would benefit from another flavor of reference: no-access. A no-access reference would guarantee the referent's existence but could coexist with shared and with mutable references. Safe code would be unable to read or write through such a reference. Other than making some cell-like types mildly less mind-bending, I'm not convinced I have an actual justification for this thing. This would give Rust three flavors of references.
But I can imagine a Valen-like language having three flavors of references: frozen references (cannot use them to mutate and there's a promise that no one else can either), exclusive references (fully mutable, etc, just like Rust's &mut) and flexible references (the kind of reference in the blog post).)
Short answer: Valen would have something similar to Fn and FnMut (but phrased in terms of effects rather than Fn vs FnMut). In other words, we would be able to express "a closure that does not modify anything it captures", or rather, "a closure that has no mut effects".
That closure, because it doesn't modify anything it captures, would be safe to share among multiple threads in a structured-concurrency-like / std::thread::scope-ish way.
The key here is that one _can_ express immutable references in Valen; an immutable reference is a reference that the containing function doesn't express a `mut` effect for. And once we have immutable references, we get all of the nice concurrency benefits that Rust trailblazed.
I'd also like to make a way to do the above without a function call, perhaps using something like the `parallel` keyword I described in [0].
A no-access reference is an interesting idea. That could be a more powerful way to express may_dangle. In Valen, I hope to have an "opaque" group to express something like that.
I don't know whether it's a good idea, but in Valen I'm trying to decouple access capabilities away from the reference types as much as possible. We'll see if that bet pays off.
[0] https://verdagon.dev/blog/seamless-fearless-structured-concu...
I'm contemplating this. Is it enough?
Suppose I have an object (I'm not even trying to get the syntax right, especially since Valen's syntax appears a bit different from Rust's):
And I also create a structured concurrency thingy in the same scope: Now I pass references to both of these down the callchain, through a few functions, maybe via some structs with lifetime parameters, and in the inner function I do this: where print_in_rust is a Rust function taking &T. (I haven't the faintest clue how to spell that in Valen.) So I'm making a closure, and the closure captures obj, and the closure needs obj to exist and be immutable for the lifetime of the closure, which exceeds the creating function's lifetime. It's bounded by the workgroup's lifetime, and Rust is fine with this.But, if I'm understanding you right, the immutability of the referent of obj depends on the signatures of everything in the callchain that might execute during the lifetime of the closure. How does that work?
edit: On further contemplation, I don't think that actual concurrency is needed to illustrate it. I think the same issue exists if I have a T<'a> that has a method that takes an &'a reference (probably like store_a_reference(&mut self, ref: &'a u32)) and dereferences ref both immediately and later and asserts that it sees the same value both times.
In Rust, a reference is forever shared/immutable or forever unique/mutable.
In Valen, a reference is... "it depends". Specifically, it depends on the context.
It's similar to a &GhostCell<T>, where its mutability isn't determined yet because the GhostToken isn't present yet.
So, what determines the mutability at any given point in time? The function's `mut` effects (or lack of them) for the group/path that the reference is pointing to.
So we can imagine a `execute_on_4_threads` function like this:
(`Func<void, (), C>` is a trait for a function that returns void, takes no extra parameters, and names its captures as group C).The most relevant fact here is that this function doesn't declare any `mut` effects at all (not on C, not on W's group, nothing), so nothing is being mutated. (And because of that, this function's callees also can't have any `mut` effects; they also can't mutate the data)
Now let's say we changed `workgroup`'s type to `&W in w`, and added a `mut(w)` to the function, to describe that we might modify the workgroup.
At that point, we would still know that we can invoke the closure from 4 threads, because we declared no relationship between `w` and `C`, so the compiler assumes (and enforces) that they're disjoint, have no overlap, nothing in one aliases anything in the other.
Hopefully that helps. Maybe I should write a blog post on this, my posts are usually clearer than my HN comments.
(Also, I'm not sure I understand your edit, if you could clarify that would be much appreciated)
My point is that Receiver::set() requires a promise that its parameter's referent is immutable for the entire lifetime 'a, which exceeds the lexical duration of set() itself. And valen_outer_fun in Rust knows this to be true as a result of Rust's shared-xor-mutable system, and Rust's borrow checker verifies this:
a) the code would compile if I removed the offending *val = 17
b) the code, correctly, does not compile as written
c) If I force the issue by replacing *val = 17 with:
then it panics and we can all imagine we're using C++ instead of Rust :)But valen_inner_fun does not mutate val, and if I understand right then this means that Valen would accept its Valen equivalent without mut(val), and this seems unsound to me.
When I first thought of this, I imagined set() instead being a work-dispatching function that would run its passed-in closure and hit UB due to a data race with the *val = 17 line, and my edit was my realizing that there's a simpler non-concurrent example.
Rust solves a similar problem in closures with unique immutable references, but they're not quite the same.
Unsafe Rust could promote a no-access reference to a shared or a mutable reference, and the unsafe code would be responsible for not violating exclusivity rules but would have a guarantee that the referent actually exists. Using unsafe code to create a no-access reference to a nonexistent object or to a misaligned object would be UB.
Safe code could convert the other way:
This is not an entirely serious proposal.So for example:
Hence the questionYour example is sneaky, though. Lifetime issues aside (suppose the next line of code uses b), that’s a noaccess reference to memory (an object? a place? I’m not sure what the current term is) that is only guaranteed to exist so long as data is not mutated. So the code with a subsequent use of data would error.
But if it were instead:
Then it would not error.> So we spent months sharpening the explanation and figuring out its benefits, and wrote a post 8 on it last year, hoping other languages would pick it up.
My language (see my submissions) has this feature since many years. In its current form it exists roughly since year 2020, but some under-the-hood changes and slight improvements have been done since.
That's the one line that defines what's going on here.
If all you want is "no use after free", either reference counts or garbage collection will do the job. That's how most GC-oriented languages work.
Rust enforces stricter rules - single ownership, one writer or N readers. The question is whether that's worth the trouble. In threaded programs, it definitely is, because it prevents race conditions. Whether it's a win for single-thread programs is a good question.
Vale seems to be a way to move much of the overhead of a reference-counted system at compile time. Nothing wrong with that. The main motivation seems to be better interoperability with less restrictive languages. Am I reading that correctly?
I'd describe this as approximately weakening aliasing xor mutability to interior-aliasing xor mutability. The motivation seeming to be to have a more expressive system that rejects fewer correct programs. With a secondary goal of interacting with more (not less) restrictive languages (namely rust).
Edit: tfa makes it clear it's a separate project
Also, I really like where Vale ended up, Vale's generational references + region borrowing was a truly weird and unusual memory safety blend. I didn't want that combination to be lost to time, so I wanted "Vale" to keep referring to that.
I hope they one day upgrade to full group borrowing, it would be a nice fit for them.
[0] https://verdagon.dev/blog/group-borrowing
Having mutability not be a property of the data, but of the function arguments reminds me a lot of modes from Jane Street’s OxCaml ^1 which is really interesting to me, as OxCaml’s focus is not really about memory management (they still use garbage collection for everything not on the stack). Feels like we might be converging towards a new standard! I can see the morning sun on the horizon :)
[1]: https://oxcaml.org/documentation/modes/intro/
I don't know if you were aware of this, but Rust has that too. (Actually it's a property of any binding, not just function parameters)
And Rust already supports forms of mutable aliasing already, such as with Cell, and GhostCell which is halfway towards Valen's approach. I would love to see someone augment GhostCell to have the sort of "invalidation" logic that group borrowing does. In fact, Plecra is working on something like that with their (WIP) "Exclusion Typing". [1]
On top of that, Polonius already thinks in terms of "paths", just like Valen does. So it's not so much a question of compiler design, but of language design, and how Rust would expose it to the user.
[0] https://smallcultfollowing.com/babysteps/blog/2024/06/02/the...
[1] https://dilated.me/blog/posts/introduction-to-exclusion-typi...
let my_ref &Entity in (live_list[], dead_list[]) = if ...
though it would be nicer in practice, because the compiler could infer that.
Haven't implemented it yet of course, but the data structures in the borrow checker are designed with this in mind.
Edit: it turns out, Ante has path unions! https://www.reddit.com/r/ProgrammingLanguages/comments/1x37b...
That wouldn't be shareable with another thread, because part of it is `mut`. _Theoretically_ we could make this shareable with other threads if we had another kind of effect, let me know if you're curious about that.
The `entity in world.entities[] mut` gives `step` blanket permission to mutate any element inside world.entities. We wouldn't be able to say "all other entities are readonly".
Though, it's worth mentioning that `advance` and `resolve` both receive the entity as a unique reference (since it's the only reference into a group which has a mut effect).
If Valen solves those challenges as well as I think it can, it could be a good direction for them to explore.
The problem with this is that it imposes a cost on abstraction. Zero-cost abstraction is one of the most fundamental design principles behind both C++ and Rust.
A field-path into a struct/enum fundamentally depends on its concrete implementation. If you hide the fields of a struct and use getters/setters, you break the ability to talk about the "paths modified" by a function which uses getters/setters.
Aside: it really annoyed me that I had to read halfway through this article to find the first attempt at defining "group borrowing". The whole first half of the article is basically fluff.
There are multiple links in the article that suggest the reader skip to the good stuff if they are already familiar with how borrow checking works