~icefox/garnet#65: 
Borrow checker design

On borrowing temporary values: https://blog.m-ou.se/super-let/

Status
REPORTED
Submitter
~icefox
Assigned to
No-one
Submitted
1 year, 3 months ago
Updated
4 months ago
Labels
T-DESIGN

~icefox 1 year, 3 months ago

Syntax is a pain so let's trivialize it: We have two types, Share('a, T) and Uniq('a, T), for shared borrows and unique borrows respectively. Unique borrows can be mutated, shared cannot.

~akavel 1 year, 1 month ago

Not sure if you've seen it, but IIUC the new post from Niko Matsakis may be of interest here: https://smallcultfollowing.com/babysteps/blog/2024/03/04/borrow-checking-without-lifetimes/ (or not? the contents definitely flies over my head; but I just immediately thought it could possibly be interesting to you in context of Garnet)

~icefox 1 year, 1 month ago

I did see it, but this reminded me to look at it again. Thanks, it's very promising. Doesn't get too deep but it says there will be a follow-up, so I look forward to it.

On March 5, 2024 9:39:40 AM EST, ~akavel outgoing@sr.ht wrote:

Not sure if you've seen it, but IIUC the new post from Niko Matsakis may be of interest here: https://smallcultfollowing.com/babysteps/blog/2024/03/04/borrow-checking-without-lifetimes/ (or not? the contents definitely flies over my head; but I just immediately thought it could possibly be interesting to you in context of Garnet)

~icefox 9 months ago

And the promised followup: https://without.boats/blog/pinned-places/

Most interesting stuff at the end, under "for the next language". Basically this proposes removing the ability to move out of &mut references and adding another reference type, so with increasing power we would get &, &mut and &pin where only the last could be moved out of. So the increase of power becomes borrow -> mutate -> move.

That then makes me ask what the difference is between an owned value and a value that can be moved out of, so. Need to reread it.

~icefox referenced this from #67 7 months ago

~icefox 7 months ago

Another useful reference: https://sabrinajewson.org/blog/null-lifetime which starts hypothetical but goes to interesting places pretty quickly. That leads to https://internals.rust-lang.org/t/opposite-of-static/5128 , which is... weirder.

~icefox 7 months ago

Found a case to think about while playing with syntax in core/convert.gt:

fn try_from_from_assoc_types(|In| from_impl From[In]) TryFrom[In] =
  TryFrom(|In| {
    type Self = from_impl.Self
    type Err = Never
    .try_from = fn(input In) Result[Self, Err] =  -- <-- in Rust this would have to be `move |input: In| -> Result<Self, Err> { ... }`
      Result.Ok(from_impl.from(input))
    end
  })
end

Basically the question is, do all closures take their environment by reference like Rust, or do they move it by default (what I was implicitly assuming here, since everything is Copy in the current implementation, including From). I haven't thought about this too hard yet, I've just been saying "we'll do it like C++ even if it feels wrong, and have the ability to specify what the environment of a closure is and how it borrows the things in it" and brushing off details for later.

~akavel 7 months ago*

FWIW, I recently stumbled upon a currently Not Accepted writeup for a proposed 2024H2 plan for Rust's "ergonomic initiative". I found it interesting in noting some pain points that I recognized as indeed things I encountered but never fully consciously named before:

https://rust-lang.github.io/rust-project-goals/2024h2/ergonomics-initiative.html

An interesting related writeup is about "Ergonomic ref-counting" which was "Accepted" apparently for H2 (though it's a pre-design phase IIUC, more of a statement of area of interest):

https://rust-lang.github.io/rust-project-goals/2024h2/ergonomic-rc.html

I think I saw a longer discussion of this on some issue or somewhere, or some blog maybe (? or on lobste.rs?), but I can't find it now unfortunately. Or maybe I just imagined it or conflated with something else? πŸ€” (may or may not be through this discussion/flamewar: https://lobste.rs/s/6usfpd/swift_is_more_convenient_rust but still can't find it there - but linking anyway as it may be interesting to you possibly anyway)

~icefox 7 months ago

Thanks! I've seen lots of those ideas in bits and pieces but not all in one place. Possibly you're looking for some of withoutboat's writings? They have some good shit on "a nicer Rust": https://without.boats/blog/notes-on-a-smaller-rust/ https://without.boats/blog/revisiting-a-smaller-rust/. Here's more or less my thoughts so far:

Any implicit or automatic RC is going to be a hard sell to me. The Garnet runtime model should not assume the presence of refcounting or a particular style/implementation of refcounting. I think borrow checking + easy RC is a great idea for a different language, but Garnet is here to write that language's runtime libs. Now, the "ergonomic RC" proposal isn't actually proposing much of anything yet, it just describes the problem (pretty well, too). But I don't see a way to solve the problem without making RC's privileged in the language. To me a lot of Rust's value comes from the compiler doing what you tell it to do, whether or not it's convenient.

For the ergonomics initiative thing, reducing unwrap()'s definitely is something I want to do. Or rather, hiding unwrap()'s, 'cause that's what they're really talking about. I was low key intending to just have a ! operator that panics on Err, but there could be other good options. A probably-bad-idea just occurred to me: maybe have a ! operator that acts like ? if the function returns a Result, but panics if it doesn't? That gets you the nice ability to being able to tell if a function may panic or not by looking at its signature, but being able to change it from panicking to non-panicking without having to go through it and refiddle a bunch of error handling. Zig's error types are another interesting point in the design space for that; I don't like them, but they seem to work.

Partial borrows for structs seem hard. There's reasons that Rust closures can infer these borrows but normal functions can't have them specified. IIUC you would need a way to write a function signature which borrows specific subfields of a struct, which starts getting into row polymorphism shiz. It would be really nice, but I do not have the type-fu to want to tackle that.

Named and optional args is also worth keeping an eye on. Rust currently handles this very effectively and very tediously with the builder pattern. I am all in favor of effective tedium over ineffective magic. But there should be no reason the tedium can't be automated either; IMO Rust's problem is that writing a macro to implement a builder for an arbitrary struct is too damn hard. I recently had a take on this sort of thing with the visitor pattern, and I do think builders are another place where Garnet's more orthogonal type system and more reflection can take the "effective and tedious" approach and make it less tedious without fundamentally changing it.

~akavel 7 months ago*

I'm absolutely not in any way into PL design, so I'm afraid I can't help you with any of that sorry; I'm just here to throw things your way and see what sticks 😜 and maybe sometimes by some chance my comment helps you in something, or at least helps you clarify your ideas - now that I think, I'm more than fine if it might sometimes happen purely due to me having the honor of being your rubberducky 😜

As for Zig: I like parts of the idea I think, but I hate that one can't pass the error details/context with the error code. I got totally spoiled by Go in this area, and I struggle with this even in Rust actually. (Rust can fortunately carry the details, but adding them is something of a fuss; but that's for another story I guess.)

As for Garnet being here to write the language's libs, I'm all for that. That's why I'm here, and AFAIU so are you in the first place, to see you make Ze Better Rustβ„’ for me for completely free. (You're such a nice guy, man!)

As for unwraps etc... this kinda reminds me of my pipe dream on the angle of PLs - not sure I ever wrote you about it... not sure that's the right place to do it either, but if we're already at a tangent with you commenting on all the items of the ergonomics initiative post ;P I'll take it as an excuse to let myself continue piggybacking ;P I promise it will be somewhat relevant!

#My Pet Idea For a PL

So, the idea I like to think of is for a language that would be kinda "gradually typed", but with this idea taken even further: allowing me to use the same language to code at any point of the following rough "spectrum":

dynamically typed (like Py/JS/Lua/...) <-> statically typed (Go/Java/...) <-> borrow-checked (Rust) <-> formally proven (Dafny/DrNim-but-real/Fstar/etc)

Notably, my "use case" is that I'd like to be able to start coding my prototype in a completely loose, freeform, fast to write dialect of the language - the dynamically-typed/Lua-like one - so that I could quickly experiment and PoC a "happy path". Quickly get to a very broken prototype, that sometimes works though, and lets me quickly check if what I wrote even validates my idea, is it helping me, or does it have some major problems so that I need to scrap it completely and get back to the drawing board.

Then, if the PoC kinda shows up to make some sense, I could start chiseling on it to make it statically typed. Ideally, I imagine gradually fencing some areas of my code with some kind of #begin-statically-typed ... #end-statically-typed pragmas, such that the static typing check would be enforced inside. Eventually, I would highly probably mark whole files statically typed, maybe only some of them, maybe all of them. If the latter, I'd maybe eventually put a "default-mode=statically-typed" flag in my "cargo.toml" file, and then "reverse the polarity" - maybe still allowing dynamic typing in some small rare enclaves of #begin-dynamically-typed ... #end-dynamically-typed. In a way, kinda how Rust does with unsafe.

Similarly, in a next phase I might like to start gradually enforcing borrow-checking strictness.

Finally, I imagine wanting to be able to mark some areas of code as required to be formally proven. But given how much effort this could take, I totally assume it would only happen for some areas of the code: stuff like crypto, or money-related maybe, or just where I wanted to have some fun. But who knows, maybe some day all of it? or at least most?

The point is, currently I need to jump languages to do that, which has the disadvantage that it requires me to rewrite stuff from scratch when I jump from one level to the next, and rarely allows me to use those different phases together - which kinda means I don't really do it often.

With all that said, I got a thought that "automatic unwrapping" or "automatic refcounting" could maybe be done through a kinda similar "fenced region" of enforcement/auto-application. Per the "ergonomics initiative" doc, the idea is presumably I'd prototype with auto-unwrapping applied on some region of code (I could mark it with those fences in my dream language), but then later disable this "region" and switch back to explicit handling. In fact, the benefit here over ! to me is, that for !s I need to grep them manually, whereas if I enable/disable a region, I'd have the compiler point out all the auto-unwraps for me, and force me to fix them.

FWIW, I think this is kinda similar to what C++ ppl tried to discuss at some point, and maybe still do (it could have come from Bjarne himself, but not sure).

~icefox 5 months ago

For reference to general Cell-like stuff design, rpjohnst has some interesting thoughts on RefCell: https://www.abubalay.com/blog/2020/01/05/cell-field-projection

Related is Verdagon's wild and winding traipse through the design space: https://antelang.org/blog/safe_shared_mutability/

Turns out making Cell able to contain non-Copy things is easy: https://github.com/rust-lang/rfcs/blob/master/text/1651-movecell.md You just aren't allowed to call get() on them, only set() and replace(). Not the most useful thing ever, but certainly not magical.

~icefox 4 months ago

I've finally sat down and proven you can totally make a safe-but-limited borrow checker, with a couple relatively simple invariants: references may not live on the heap, and references on the stack may only point at things longer-lived than they are -- ie, they may only point up the call stack. It's not quite trivial, you have to make sure mutable references aren't mutated to point to something younger than themselves. But it seems like a good first step.

Register here or Log in to comment, or comment via email.