Pre-pre-RFC/Working Prototype: Borrow-Aware Automated Context Passing

There are two types of borrow information a user could care about: whether a capability could be borrowed and whether a capability is actually borrowed. Here's an example which should illustrate the difference:

cap! {
    SubSystem1Values  = Vec<u32>;
    SubSystem1Flag = bool;

    SubSystem2Values  = Vec<i32>;
    SubSystem2Flag = bool;
}

mod subsystem_1 {
    use super::*;

    pub fn foo() {
        for &v in cap!(ref SubSystem1Values) {
            bar(v);
        }
    }

    pub fn bar(v: u32) {
        if v == 42 {
            *cap!(mut SubSystem1Flag) = true;
        }
    }
}


mod subsystem_2 {
    use super::*;

    pub fn foo() {
        for &v in cap!(ref SubSystem2Values) {
            bar(v);
        }
    }

    pub fn bar(v: i32) {
        if v == -42 {
            *cap!(mut SubSystem2Flag) = true;
        }
    }
}

fn user() {
    let old_value = *cap!(ref SubSystem2Flag);
    subsystem_1::bar(42);
    assert_eq!(old_value, *cap!(ref SubSystem2Flag));

    let old_value = *cap!(ref SubSystem1Flag);
    subsystem_2::bar(-42);
    assert_eq!(old_value, *cap!(ref SubSystem1Flag));
}

It is important that subsystem_x::foo() know that subsystem_x::bar() does not actually borrow SubSystemXValues mutably since that allows it to iterate through it while calling subsystem_x::bar(), even though bar technically could. However, the user function does not really care which specific subsystem-internal state the system_x::bar() actually touches, so long as it couldn't touch other subsystems' internal state.

The actually borrows information is only useful for getting a program to borrow-check. Hence, I think it's fine (and, indeed, quite valuable) for that information to be left implicit. The could borrow information, meanwhile, is very valuable to code reviewers since it helps make semantic sense of the program but doesn't really mean much to the borrow checker, hence why it has no effect on the borrow checker.

This is why I think my realms proposal could effectively tame the implicitness of this proposal. I go into a lot more detail about it in my still WIP RFC but, the gist of it is that every single capability (which I call "contextual parameters" in my RFC to avoid the whole "capability doesn't mean that" bikeshed) belongs to a realm representing a subsystem (or sub-component of a subsystem) and functions that wish to access that context must declare the fact that they're operating in a realm which has access to that context:

realm Subsystem1;

realm Subsystem2;

realm App: Subsystem1, Subsystem2;

ctx SYS_1_VALUES in Subsystem1 = Vec<u32>;
ctx SYS_1_FLAG in Subsystem1 = bool;

ctx SYS_2_VALUES in Subsystem1 = Vec<i32>;
ctx SYS_2_FLAG in Subsystem1 = bool;

fn foo() use Subsystem1 {
    for &v in SYS_1_VALUES.iter() {
        bar(v);
    }
}

fn bar(v: u32) use Subsystem1 {
    if v == 42 {
        *SYS_1_FLAG = true;
    }
}

fn baz() use Subsystem2 {
    for &v in SYS_2_VALUES.iter() {
        quux(v);
    }
}

fn quux(v: i32) use Subsystem2 {
    if v == -42 {
        *SYS_2_FLAG = true;
    }
}

fn user() use App {
    let old_value = *SYS_1_FLAG;
    quux(-42);
    assert_eq!(old_value, *SYS_1_FLAG);

    let old_value = *SYS_2_FLAG;
    bar(-42);
    assert_eq!(old_value, *SYS_2_FLAG);
}

Realms are designed to be watertight: if a function requests a limited realm, it cannot suddenly gain access to a larger realm. This gives the code-reviewer of this program a very useful guarantee: baz and quux cannot modify state affecting foo and bar and vice versa.


Clarifiction: this is true of private APIs only! Public APIs still need to be aware of both potential and actual borrow sets, which is why I'm including the #[borrows_only] attribute proposal in the RFC.

2 Likes