[HN Gopher] From Stacks to Trees: A new aliasing model for Rust
___________________________________________________________________
From Stacks to Trees: A new aliasing model for Rust
Author : obl
Score : 80 points
Date : 2023-06-10 08:12 UTC (1 days ago)
(HTM) web link (www.ralfj.de)
(TXT) w3m dump (www.ralfj.de)
| saurik wrote:
| > ...by the time x.len() gets executed, arg0 already exists...
|
| So, I realize that this is the way that Java does it--and,
| presumably, one still doesn't get fired for doing whatever Java
| does ;P--but, would it not actually make more sense for the
| arguments to be evaluated before the target reference, making the
| argument order more like Haskell/Erlang (but very sadly not
| Elixir, which makes it awkwardly incompatible with Erlang and
| breaks some of the basic stuff like fold/reduce)? Particularly
| so, given that, as far as I can tell from this example, what
| makes arg0 have the type that it does is the type of the function
| that hasn't even been called yet? (As in, the semantic gap I am
| seeing between what the user probably meant and what the compiler
| wants to do is that "x" shouldn't really be mutably-borrowed
| until the call happens, and the call here clearly shouldn't
| happen until after the arguments are evaluated.) (Note: I do not
| program in Rust currently; I just have spent a number of decades
| analyzing languages and at times teaching college language design
| courses. I might be missing something obvious elsewhere that
| forces Rust to do this, but that one example, in isolation, at
| least feels like an unforced error.)
| Georgelemental wrote:
| In Rust, `reciever.some_method(whatever)` is supposed to be
| relatively thin sugar for
| `TypeOfReciever::some_method(receiver, whatever)`. So the
| evaluation order should be the same for those two forms.
| MuffinFlavored wrote:
| fn two_phase(mut x: Vec<usize>) { let arg0 = &mut x;
| let arg1 = Vec::len(&x); Vec::push(arg0, arg1);
| }
|
| > This code clearly violates the regular borrow checking rules
| since x is mutably borrowed to arg0 when we call x.len()! And
| yet, the compiler will accept this code
|
| Does anybody else wish the compiler wouldn't and would be even
| more verbose? I know one of the biggest learning curves
| (personally) for Rust is the borrow checker complaining hardcore
| and "getting in your way" preventing you from basically doing
| anything you're used to (passing around pointers in C or objects
| in JavaScript (even though you should be following immutable
| practices and not doing object mutation... most of the time))
|
| I'm sure there's probably been discussions on how to make the
| borrow checker less "mean/rigid/obtuse" but silently passing
| something as "non mut" and it actually does "mut" stuff, I
| wouldn't have guessed Rust allowed that.
|
| Edit: gah, I did not realize the function signature is (mut x), I
| thought it was just (x) and the mut was implied which is what I
| was trying to call out, apologies.
| Osiris wrote:
| I've been learning rust and I spend the vast majority of my
| time dealing with lifetimes and borrow checking. Common ways in
| used to doing things simply don't work in rust and a lot of
| effort has to go into keeping track of how and where data is
| used.
|
| I've worked in OOP languages, functional languages, and dynamic
| languages but all of them were essentially garbage collected,
| so having to keep track in my head of how data ownership is
| managed is a big learning curve.
| imron wrote:
| As a c++ programmer, one of the great things about rust is
| that I no longer have to keep track of data ownership and
| management in head.
|
| I can outsource this to the compiler and if I get it wrong
| the program won't compile.
|
| In c++ you still need to do all the same tracking and
| management if you want safe and correct programs, but you
| don't get nearly as much help from the compiler if you make a
| mistake.
| FpUser wrote:
| I think this is largely overblown if one uses modern C++.
| One of the things I do is stateful multi-threaded business
| servers and frankly comparatively to the overall project
| this "data ownership maintenance" is small to the point of
| being practically absent.
| denotational wrote:
| The original code (which desugars to the snippet you posted)
| is: fn two_phase(mut x: Vec<usize>) {
| x.push(x.len()); }
|
| This should clearly be accepted (this is self evident in my
| opinion); if you need to jump through loops to write code like
| this then the language is too restrictive to write normal code.
|
| The standard implementation of Rust does indeed accept this,
| and there is no soundness hole here.
|
| The existing semantics for aliasing and borrowing from MPI
| (Stacked Borrows) don't allow this, which means the semantics
| are overly restrictive; we _want_ this to be accepted.
|
| This work "fixes" this issue by extending the semantics to
| admit the behaviour exhibited by the standard implementation.
|
| The rules for the borrow checker are not fully formalised and
| to some extent the rustc implementation _is_ the specification;
| formalising the rules (i.e. RustBelt, Stacked Borrows, etc.) is
| important, but we don't want to formalise something that is
| strictly more restrictive than the reference implementation,
| especially if there's no soundness hole.
| FpUser wrote:
| >"Does anybody else wish the compiler wouldn't"
|
| Compiler being obtuse and not being able to figure when it is
| safe to "break rules" is the problem. Not twisting brain of the
| programmer into being "safe compiler". This sounds like a
| Stockholm syndrome.
|
| >"you should be following immutable practices"
|
| No I should not. I should do what makes sense in particular
| situation and not bending over for some zealots trying to
| enforce one and the only way.
| neerajsi wrote:
| I think one measurable outcome here is what kind of error
| message you get when you do violate a rule and whether rust
| users know what to do to fix their code. As a person who
| loves to explore the complexity behind seemingly simple
| interfaces, this stuff is really cool. On the other hand, I
| don't relish having people break their brains to understand
| why similar code is accepted vs not.
|
| I'm not a rust user myself, but I'm guessing from all the
| references to raw pointers that a lot of the code referenced
| here is actually not idiomatic for all but small snippets of
| high perf code, so maybe the complexity is not going to
| affect too many people.
| FpUser wrote:
| >"so maybe the complexity is not going to affect too many
| people"
|
| I think this statement shows a high level of disrespect for
| users.
| Ygg2 wrote:
| > Compiler being obtuse and not being able to figure when it
| is safe to "break rules" is the problem.
|
| Compiler afaik will never be able to correctly 100% identify
| you are or aren't breaking some properties due to Rice's
| Theorem.
|
| That said, you're committing a Nirvana fallacy. Perfect
| doesn't prevent improvement.
|
| E.g. seatbelts don't prevent being stabbed by a large metal
| pole, ergo it's useless.
| TazeTSchnitzel wrote:
| > silently passing something as "non mut" and it actually does
| "mut" stuff
|
| No, it's the opposite that's happening here: a mutable borrow
| of the vector is made, and then a non-mutable thing is done
| with it (getting the length), before finally mutating it
| (pushing).
| wongarsu wrote:
| The borrow checker was made for correctness, not correctness
| for the borrow checker.
|
| You have ownership of a Vec, you get its length, then you push
| to it through a mutable reference; nothing evil happens here
| except the order of the statements (which is an implementation
| detail that people might not think about when writing the short
| form x.push(x.len())). The code above is perfectly safe if
| written in C, which is why the borrow checker was extended to
| also allow it in Rust. You could make the argument that simpler
| borrow checker rules lead to a simpler mental model. The
| counterargument (that won in the end) is that "if it's safe,
| the borrow checker allows it" is a mental model worth pursuing.
| saghm wrote:
| If I'm understanding correct, the major change here for Rust
| users (rather than people who hack on the compiler) is that
| mutable references will not be considered to be "interfering"
| with other references being made at the same time until they're
| actually written to for the first time. This makes intuitive
| sense to me, but I suspect that there may be a bit of concern
| that this will make things more confusing when reading code and
| trying to understand what's going on. I'd be lying if I said that
| thought didn't occur to me, but at this point being surprised at
| how much I end up liking the way things turned out has become the
| norm for me; I remember having misgivings about nested import
| paths (rather than only being able to use `{`...`}` at the very
| end), match ergonomics, and `.await` as a postfix keyword but
| pretty quickly became glad they decided things the way they did
| after using each of them a bit when they finally got stabilized.
| I think I did realize that I'd like NLL (i.e. the borrow checker
| detecting the final use of a reference and not considering it as
| conflicting for the remainder of the scope) before it landed, but
| I know a lot of people had misgivings about that as well. I
| imagine this will be one of those things that in a few years will
| seem weird it wasn't always how it worked!
___________________________________________________________________
(page generated 2023-06-11 23:00 UTC)