Borrows in generic code can violate uniqueness constraints
yorickpeterse opened this issue · comments
Please describe the bug
When using generic code, it's possible to borrow a generic type parameter that ends up being assigned a uni T
value, which then allows the borrow to outlive the unique value and violate the uniqueness constraints.
Please list the exact steps necessary to reproduce the bug
Consider this code:
class Example[T] {
let @value: Option[T]
let @borrow: Option[ref T]
fn mut set(value: T) {
@borrow = Option.Some(ref value)
@value = Option.Some(value)
}
}
class async Main {
fn async main {
let ex = Example(value: Option.None, borrow: Option.None)
ex.set(recover [10, 20])
}
}
Here ex
ends up being of type Example[uni Array[Int]]
, with the borrow
field storing a ref Array[Int]
, violating the constraints.
I'm not sure yet what the solution here is. If we introduce something similar to the mut
constraint for type parameters to allow unique values, then we basically end up not being able to use them anywhere or we won't be able to use borrows. Basically it ends up being a massive pain.
Operating system
Fedora 40
Inko version
main
Rust version
1.77.2
In theory something like this would work: when type-checking a method body, we set some sort of "borrowed" flag for type parameters whenever they're borrowed, either directly or by passing it to an argument of another method that borrows it. When we then assign a uni T
to such a type parameter, we disallow this at the call site.
The problem with this approach is that it requires at least two passes over all method bodies: one to set the flag for direct borrows (e.g. through ref x
expressions), and one to set the flag for indirect borrows. I'm also not sure this analysis would always be complete.
Here's another way this can be triggered: if you have some uni T
and do that_value.some_field.iter
, the returned Iter
produces values of type ref Something
, allowing you to violate the uniqueness constraints by storing those ref
values somewhere.
Another similar case: we allow pattern matching against e.g. Option[uni ref Something]
, which then lets you introduce a borrow by binding the uni ref Something
to a match variable:
match something {
case Some(v) -> {
move_unique_value_to_another_process(unique_value)
do_something_with_the_uni_ref_we_still_have(v)
}
...
}
To prevent this from happening we have to disallow binding uni ref T
and uni mut T
values to match variables, similar to how you can't assign them to locals.