[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)