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

Hmm...

It feels like a pretty comparable situation to borrow-checking, though.

If you have:

// (using a type-alias style of definition since that seems to be
// more desired by users on this thread)
realm MyRealm = MY_CX_1, MY_CX_2, MY_CX_3;

ctx MY_CX_1 in MyRealm = Vec<u32>;

fn foo() use MyRealm {
    for _v in MY_CX_1.iter() {
        bar();
    }
}

fn bar() use MyRealm {
    baz();
}

fn baz() use MyRealm {
    MY_CX_1.clear();
}

Whether foo compiles is determined by the actual borrow set—not the realm—so you'd have to look at the entire function body and all its callees to determine which bits of the realm are borrowed. This isn't a problem, though, since this is an error scenario and, in error scenarios, the compiler can just point out what you're missing.

In that sense, I think partial contexts should be the same. They're an error scenario so adding the necessary context can be a task driven by the compiler diagnostic.

but it's annoying that unless the developer filled in #[borrows_only].

rust-doc will always show the actual borrow set of a function since that is very much an important part of its public API so, for API usage, in addition to getting help from the compiler, you could also just look at the rust-doc.

Also, the warnings that lint generates will tell you the full attribute to apply to your function so you could just copy paste that or have rust automatically apply the fix for you since they should be machine-applicable. Of course, you should probably double-check that set to see if there's anything else you want to add to make the function more future-proof. In that sense, it's better than existing context passing solutions since you can remove contextual parameters from the function without having to update the signature and produce a breaking change.

Yes, library authors have to commit to a borrow set as they would with any other context injection system. It's just that, here, unless you specify #[borrows_only], this set will be implicit. That's why there's a warn-by-default lint against omitting it. Unless you're actively prototyping a crate or implementing a crate that's purely an internal helper crate for other crates, you should include #[borrows_only] on your public functions.

Perhaps it would be a good idea to let borrows_only borrow an entire realm so crate authors can just pessimistically say that they'll borrow everything and loosen it later as they commit to more decisions about their crate.

It doesn't break the bounded refactors property because you only have to make this change at bind sites—not at every intermediate site between the provider of the context and the consumer of the context. We can't just make up a context for the user so this seems like reasonable work to ask from the user.

Since context can be passed from quite far away or acquired using a Bundle, you can easily design programs which minimize the number of places where you have to make these types of changes. For example, you could combine the runtime dependency injection system of an ECS with the static dependency injection of this feature to make updates really not that difficult:

use bevy_context::Resources;

// (a macro could generate these two lines)
realm Sys1 = MY_CX_1, MY_CX_2, MY_CX_3;
type Sys1Res = (Mut<MY_CX_1>, Mut<MY_CX_2>, Mut<MY_CX_3>);

ctx MY_CX_1 = MyResource1;
ctx MY_CX_2 = MyResource2;
ctx MY_CX_3 = MyResource3;

fn sys_1(res: Resources<Sys1Res>) {
    bind res.bundle();
    ...
}

fn sys_2(res: Resources<Sys1Res>) {
    bind res.bundle();
    ...
}

fn sys_3(res: Resources<Sys1Res>) {
    bind res.bundle();
    ...
}

(...although, personally, I tend to just write out the entire resource set for each system since I can apply those changes automatically and the resulting borrow sets are smaller, which benefits automatic parallelism scheduling)

The problem is that defining super precise realms requires you to define realms based on the structure of your function rather than by some user-defined notion of what a reasonable subsystem looks like.

You'd probably have to either write your realm like this...

ctx MY_CX_1 = Vec<u32>;
ctx MY_CX_2 = Vec<u32>;
ctx MY_CX_3 = Vec<u32>;

fn foo() use MY_CX_1, MY_CX_2, MY_CX_3 {
    for _v in MY_CX_1.iter_mut() {
        bar();
    }
}

fn bar() use MY_CX_2, MY_CX_3 {
    for _v in MY_CX_2.iter_mut() {
        baz();
    }
}

fn baz() use MY_CX_3 {
    MY_CX_3.push(42);
}

...which would be precise and useful to consumers but break the bounded refactors principle, or write it like this:

ctx MY_CX_1 = Vec<u32>;
ctx MY_CX_2 = Vec<u32>;
ctx MY_CX_3 = Vec<u32>;

realm Foo = MY_CX_1, Bar;

fn foo() use MY_CX_1, MY_CX_2, MY_CX_3 {
    for _v in MY_CX_1.iter_mut() {
        bar();
    }
}

realm Bar = MY_CX_2, Baz;

fn bar() use Bar {
    for _v in MY_CX_2.iter_mut() {
        baz();
    }
}

realm Baz = MY_CX_3;

fn baz() use Baz {
    MY_CX_3.push(42);
}

...which satisfies the bounded refactors property but makes it harder to figure out what Foo actually is without using an IDE or tracing the entire realm.

In practice, since your proposed realm semantics only affect partial bindings instead of borrow checking, I think most people are just going to make larger conservative realms to get both the bounded refactors property and the readability and sacrifice the ability to make short succinct one-off contexts for the single function call.

The point of a realm is to say "this function will access a subset of the contextual parameters in this realm and nothing else." A realm being big only has the downside of reducing the number of guarantees you can derive just by looking at the function's signature since it claims that it potentially accesses a lot more than it actually accessed. If yourself in that scenario, that's probably a sign that you made your subsystem too big and should probably refactor it into multiple subsystems with their own realms. Otherwise, realms being big is a good thing since it gives you room to grow and forces the callers to be conservative about not making too many assumptions about your internal functions that may change as its implementation changes.