(Caveat in advance, I think I repeated myself a bit here, sorry)
Okay @tcsc so you think this code should be allowed
I think for some definition of "arbitrary", sure. They can't use those to have multiple &mut T of course. But something not that different than this ends up being required in some definitions of intrusive double linked lists on the stack, so yeah, I would like this to work.
// UB, the write above invalidated this raw ptr.
That line is UB because ref1
's lifespan hasn't ended yet. Once ref1
's lifespan ends, ptr1
would be usable (it's not invalidated, just temporarily restricted by the existence of that mutable reference).
unlike for C/C++, there is a precise formal spec and there are proofs that some optimizations are correct for this spec
This is nice, but in practice the downside of having to maintain the stacked borrows model in your head, (or figure out what it might be during a code review) massively outweighs this. Most programmers will not read formal models, it's nice if one exists because it means the semantics aren't completely incoherent, but I would guess most C++ programmers are happy enough with the C++ spec ā I'm not sure I've ever met one who would reach for such a model over the spec text.
So as far as constructive evidence goes, it looks like Stacked Borrows is easier to reason about than C/C++.
Yeah, in particular, easier to formally model is a totally different thing from how easy code written using it is to reason about.
While C++ has no formal model for this, when writing and reviewing code very rarely are people thinking of a formal model, instead of a set of rules, heuristics, caveats, and other such ad-hoc stuff. This is true for all models, I think. It's relatively easy to come up with a viable model like this for C++ that's probably a bit too conservative, and very likely bans many parts of the language (very easy and common in an enormous language like c++).
So, while for stacked borrows there is a formal model, it does little good, as evaluating correctness under SB requires a lot of mental overhead. You must develop the runtime "history" to reason about, which is very difficult (and hard to come up with rules to drastically simplify this that can be followed without defeating the point).
I don't think your analogy with integers works. Integers do not have provenance
Well, you may have noticed that my model tries (perhaps unsuccessfully) to eliminate alias-based provenance from raw pointers as much as possible, except during concrete, statically visible regions of time where references exist. It instead push everything onto ensuring that those references fully follow the "&T is immutable, &mut T is unique and mutable" rules.
Specifically, there's (intended to be) no permanent invalidating of a raw pointer, only temporarily restricting it, as the alternative is unintuitive, hard to reason about, and nonlocal.
That said, I do want to note that it would really surprise me very little if the approach I came up with above in around 10 minutes falls apart in some cases. I can already see cracks at the edge, but the point was more to move the discussion in this direction and start from what wants to be aggressively simpler to reason about than SB, rather than continue with discussions about stuff that won't happen and is easy to dismissĀ¹.
Specifically I wanted to push back against the idea that the solution here is to just accept SB, and find ways to live with a complex model. The goals for the model I came up with are/were basically:
-
Eliminate several of the things about stacked borrows I find impossible to reason about, or that seem too limitingĀ³
-
Specifically, it ditches as much of raw pointer provenance as I can, only keeping it if a reference exists or the rules would otherwise violate rust (e.g. writing to a non-mut non-unsafecell thing, etc).
-
Where provenance can't be removed, make it as trivial as possible to reason about:
- That is, it can be determined from reading code, without maintaining a bunch of mental state or knowing about a pointer's history.
- The current state is a nightmare auditing and reviewing, and can be very hard to explain in unsafe contractsĀ²
-
Preserve everything that is, IMO, "required" for rust to keep working as rust. E.g. don't change the semantics of &T, &mut T, ...
-
Completely ignore optimizations in most of this, with the hope that being able to apply them to code that never takes raw pointers to things is enough.
I'd like to discuss ways something that achieves some of these things is possible.
Ā¹ Like allowing multiple &mut T
s ā Additionally I also was hoping it would turn into a discussion about simpler semantics (as it has), rather than just under a flag, although I'd have accepted a flag as a conciliation prize
Ā² There's a failure mode where you end having part of the contract be "must not use the pointer in a way that violates its provenance" which is practically a tautology (i hit this a few times in my patch that made anyhow follow SB/pass under miri)
Ā³ Excluding bits that I have confidence that even under the current model will get worked out, like the size stuff, and probably ptr2int