Interior mutability by dynamic scoped borrow checking

Suppose that the Rust compiler implements a Prevents marker trait, such that impl Prevents<P> for T {} implies, during the lifetime of a stacked T instance in a thread, no P instance can be created in that thread. Not just in the current static scope - in any dynamic scope in a function call chain during the lifetime of the T instance. Prevents would (hopefully) enable a useful and flexible generalization of borrow checker rules from static to dynamic scopes.

For simplicity, suppose there is also a StackOnly trait marking stack only types. StackOnly might not be necessary, but requiring both P and T types in a impl Prevents<P> for T relation to be StackOnly makes thinking about Prevents easier (at least for me). BTW: everything here is !Sync as well.

Prevents could then be used to define a zero runtime cost, compile-time checked interior mutability cell type - PRefCell:

Any comments/criticisms of this idea?

[Those familiar with GhostCell and similar token-based interior mutability mechanisms will notice that PRefCell would have similar abilities without requiring token passing through all involved function calls or confining usage to closures, but !Sync.]

I'm contemplating what the Rust compiler might have to do to enforce the Prevents trait. I'll post about that later.

How would this be checked though? Dynamic function calls could call any function, including functions creating forbidden instances. And you also need to account for global allocators and panic hooks also being able to call pretty much any function.

That's what I'm working on now. There's a lot of function calls that the compiler would have to disallow during the lifetime of a T instance in order to enforce impl Prevents<P> for T. And it would have to be conservative because there would be some uncomputable things. But I think that the remaining set of functions that can be proven using standard static analysis techniques to not instantiate P constitute a useful set.

Consider the example at the end, especially struct LocalDummy. The compiler can tell that only functions that can see this type could possibly create the prevented type. And that's just main, so only recursive calls to main are forbidden during the lifetime of the borrows. BTW: if the compiler couldn't determine anything other than this "can't create prevented type due to lack of dummy type visibility" case, then that would make PRefCell work in most (if not all) practical cases.

During that lifetime however you print some variables, which:

  • will call methods on some trait objects;
  • can panic, calling an unknown panic hook.

You'll have to ensure that none of these call main, and that again requires non-local analysis.

That can't exist; it's not possible to create a "stack only type". You can always put any (sized) value into a Box or a Vec etc. There's no way the compiler can prevent you from doing so.

The Prevents and StackOnly traits would both require added features into the Rust compiler. Any compiler knows the difference between what is instantiated onto the stack vs. heap, it's just a matter of whether and how that knowledge is exposed to developers. Most of the time, the current Rust compiler hides that info. But it certainly knows the difference, and could be altered to require that certain types be stack only.

Using non-local analysis to get better interior mutability is the whole point of this proposal.

My initial motivation was that during translation of a small (2.5Kloc) C app to Rust, I was fighting the borrow checker because I had safe code but couldn't easily convince the borrow checker of that, until after several refactorings. In most of those cases, the original C code was safe because of very non-local rules that follow from good modularity coding behavior. One was this "non-reentrancy" of certain interfaces that made it impossible for multiple active (on the stack) mutable aliases to exist.

When I started thinking about whether using standard interior mutability such as RefCell would help, I realized that the only reason RefCell needs a runtime check is because of the potential for the same kind of "reentrancy" - for multiple stack frames in a thread to be borrowing from the same RefCell. That, combined with my experience with GhostCell (including writing my own variant), lead me to think: why not start with the assumption that the Rust compiler can help with enforcing non-local modularity of some kind similar to borrow checking, and see what happens. The Prevents and StackOnly traits are nothing more than a way to put "non-reentrancy" into a Rust source code concept. There may be other, perhaps better ways.

No. That's the point. The compiler doesn't know it, because the compiler doesn't do the allocation. Rust is not garbage collected, and dynamic allocation is not intrinsic to the language.

Explain how the compiler can tell this is an error if it doesn't know i is on the stack:

The compiler knows about the scopes of bindings, and can compare those scopes to the borrows of data owned by a binding, whether stack or heap. Note that you can get essentially the same error from a reference to a heap location: Rust Playground

3 Likes

(In fact, borrow checking even applies to zero-sized values, which do not exist in the stack or the heap or anywhere in memory at all!)

Then substitute "owned by a function" for "on the stack" in all of the things I wrote.

Explain how this is related to the stack, when the compiler produces the exact same error if you heap-allocate the value:

1 Like

That's just the current ownership system, then.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.