upvote
> 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.

reply
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)

reply
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.

reply