← Back to context

Comment by verdagon

3 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 execute_on_4_threads<C', F: Func<void, (), C>>(
      workgroup: &W,
      closure: F
    ) { ... }

(`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)

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.