-
-
Notifications
You must be signed in to change notification settings - Fork 43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Consider implementing multiple ownership and scoped borrows, replacing single ownership with runtime panics #596
Comments
To add to this: changing from our current model to this new one shouldn't be too tricky: the biggest change is enforcing that borrows can't outlive their scopes, but even there we could take a simple/naive approach of simply not allowing them in fields, return values, and by not making them compatible with owned type parameters (you'd still be able to pass them to If we enforce lifetimes per scope, it would probably come down to something like this: Each scope is given a number as its lifetime, in the range of 0..N. Each value created is given the lifetime of its surrounding scope. When returning a type from a scope or moving it (i.e. passing it as an owned value to an argument), we check if the value is or contains a reference. If so, we require that the lifetime number of that reference is greater than the scope the value is moved to. This allows passing references into sub scopes, but not into parent scopes. References can always be passed to method arguments (their lifetime is 0 because that's the outer most scope lifetime in a method), but we don't allow returning references from methods. For This is the rough idea, though I probably overlooked some important cases. Point being: references/borrows are useful if you want to take some data (e.g. |
One potentially tricky thing is that an Another issue is that of resource cleanup: if a value stores e.g. a file descriptor, then an |
In light of #778 and #776, it might be worth looking into this deeper. For example, inferring enums and tuples as value types will prove tricky, as e.g. tuples allow assignments of their fields, which won't work for immutable value types (or at the very least produce inconsistent results). At its core is the following problem: if we put data on the stack, we need a compile-time mechanism to ensure no borrows exist when said data is moved. Making types immutable value types that are copied is one approach, albeit a limited one. Swift's use of reference counting provides a unique benefit for this situation: they can make tuples and enums value types because when those types are copied, they can increment the reference counts of any heap sub values, essentially resulting in multiple ownership of those values. We only use reference counting for borrows and thus can't apply the same technique. Of course Swift's approach may result in moving of a value requiring a bunch of reference count increments. For example, for Another issue is cache friendliness: being able to shove data on the stack is great, but not if you're thrashing the cache by constantly adjusting reference counts. In our current model this happens when borrowing a lot, but in theory we can optimize a lot of that away. For multiple ownership we'd have to introduce some form of (restricted) compile-time borrowing, otherwise we'd be adjusting reference counts far too much. |
To add to the above comment: I'm pretty sure using reference counting may result in a cascade of increments when data is moved. That is, when copying outer data we may need to increment inner data recursively, otherwise if we move data out of a copy we end up with an uneven number of reference counts, which could result in an object being released prematurely. |
Description
We currently panic when dropping a value that still has references to it, and maintain reference counts when creating/dropping references. In general this works well, but the runtime panics can be hard to debug, and we won't be able to detect all dangling references at compile time.
An alternative to this approach is something I thought of a while ago but never really wrote down (I think at least):
Values start out as owned and are moved around as they are now (i.e. no reference count changes). Their initial reference count is 1, not 0.
ref T
andmut T
still exist, but can't outlive the scope they are created in. This involves tracking lifetimes/scopes internally, though we don't need to expose them to the language. In practise this means you'd mostly use these as arguments, as you won't be able to return them.Next, we introduce the expression
clone x
orshare x
(not sure yet, but I'll useshare x
for the rest of this comment), which simply increments the reference count and returns a new owned pointer. This expression works on both borrows and owned values. When dropping owned values we decrement the reference count, and drop the value if the new count is 0.In this setup, we essentially have multiple ownership, and the
share x
expression is used when returning or storing values (e.g.Array.get
would use this). While this incurs a reference count increment, this is currently also true when returningref T
ormut T
.The benefit here is that we can still express everything we can express today, but we don't have to worry about runtime panics. The downside is that we have to run destructors in a few more places, which combined with a future inlining pass may result in slightly more code being generated. The total amount of reference count changes may actually be lower than what we have today, because returning references is likely to happen less often compared to taking a reference as an argument, and those don't incur reference counts in this new model.
I believe Swift takes a somewhat similar approach, though they sort of invert things: moving values around incurs a reference count by default, whereas in our model this would be made explicit using the
share x
expression.Related work
No response
The text was updated successfully, but these errors were encountered: