Pre-Pre-RFC: Opt-in non-local borrowck analysis


#1

Summary

Introduce a modifier “nonlocal”(subject to bike-shedding) to fns and methods. Borrowck will “inline” their implementations into where they’re used before checking, which will loose the lifetime restrictions a little more.

Motivation

Nowadays, even after we’ve got NLL, certain program’s correctness still can’t be proved, putting certain limitations into API surface design.

One common issue is that: We’re not able to provide Java-style getters/setters. Any such method will put the whole value into the borrowed state, causing limitations for upcoming program actions.

Guide-level explaination

When you want to write a function that provides a “partial borrow” or something like this, you can add the nonlocal modifier on the function or method.

struct S {
   a: usize,
   b: usize,
}

impl S {
   nonlocal fn a_mut(&mut self) -> &mut usize { &mut self.a }
   nonlocal fn b_mut(&mut self) -> &mut usize { &mut self.b }
}

fn main () {
    let mut fib = S {a: 0, b: 1};
    let a = fib.a_mut();
    let b = fib.b_mut();
    println!("{}", *a);
    println!("{}", *b);
    loop {
      *a += *b;
      println!("{}", *a);
      *b += *a;
      println!("{}", *b);
    }
}

Reference-level explaination

To be written


#2

This reminds me of the “exposed body” alternative for const fn (i.e. annotate functions as “the body is part of the public API semver, and analysis should look at it instead of considering only the signature”).

cc @Centril


#3

I actually like this idea a lot compared to more elaborate and inevitably cryptic syntaxes for expressing “partial borrows” in your public API.

The problem is that this is so straightforward that I’m worried users will see the borrow check error, add nonlocal to “make the compiler happy”, and be totally unaware of the additional public API backwards compatibility guarantees they just opted into.


#4

Maybe this could be a private-only or crate-only attribute :thinking:?


#5

I would find it really impolite if a function showed its bare guts in plain sight (possibly even in the presence of underage persons watching my screen!).

In all seriousness, though: this would lead to quite surprising behavior. Functions aren’t really designed with their body being syntactically substituted in the call site in mind (and as such, taking part in their public interface). That’s exactly what macros are for, and it’s best to keep these two different modus operandi very clearly separate.


#6

Locality of the borrow checker is intentional to avoid making functions’ implementations their public interface (since merely accessing a new field in your method could break someone else’s code elsewhere). In the trivial case in your example of course that’s not a problem, but there’s nothing stopping someone from exposing a large, complex function.

I would love if private functions were automatically “non-local” within their crate or module, without need for annotations. The “public” interface of private functions isn’t exposed widely enough to cause as much trouble.


#7

That’s actually a good point. I thought locality of borrowck was due to performance reasons. If performance is not an issue, making the private- and crate- level functions enter this analysis automatically would be great :heart:


#8

I think even for private functions. I’d rather this was opt in.

I like the signature being my main reference. even as I look through my own code.


#9

I would like to express this relation ship in the function signature. For example, something like this:

impl S {
    fn a_mut(&mut self) -> &mut usize
      where 'return : 'self.a 
    { &mut self.a }
}

#10

That seems like a much more sound and generally nicer approach.


#11

It feels to me like this would be nice as an experimental RFC.

pub nonlocal goes right against the core design principles of rust, so I like the idea of nonlocal only working on non-pub functions… but there is the question of how much pressure it will exert when you try to take an existing private API and make it public. In particular, I’m worried about what happens when you try to factor out a module into a new crate; this change might encourage code duplication across crates, so that both crates can have the benefits of nonlocal.

But I don’t think we can really measure the danger without at least giving it a try.


#12

I agree. I’ve always thought the function signatures in Rust were to be treated as contracts. This design seems obvious and straightforward to me when reading. Though I’d prefer if the 'return lifetime was also attached to the returned reference so it’s clear which reference it’s referring to, or would 'return be a reserved lifetime name like 'static? If the latter, is it possible to do that now since this syntax doesn’t exist yet, or would it have to be gated until the next edition?


#13

Could this be considered with const generics? I’m thinking how amazing it would be if we could write something like

impl<n: usize, T> Vec<n, T> {
    fn split_at_mut(&mut self, i: Bounded<m, n>)
        -> (&'left mut [T; m], &'right mut [T; n-m])
        where 'left : 'self.data[0..m], 'right: 'self.data[m..n] { ... }

Where the implementation could be entirely safe Rust.