Alright I hacked together an example – BorrowChecker.jl:
It is very manual; it basically requires the user to use the macros to do anything. Maybe in the future that code_escape could do parts of this automatically though.
Note that you can always unwrap owned values with @take (which trips moved to true and thus the owned value can’t be used anymore). Instead, you should use @ref inside a @lifetime block – those create immutable references to the object (by “immutable” I just mean that setproperty! will error - not truly immutable).
And there are very few methods implemented for Owned* and Borrowed* apart from a few array methods, so don’t expect it to work apart from some simple example code:
Internally it’s calling this function request_value(var, Val{mode}) for mode in :read and :write. Basically you need to specify whether a method is mutating or not for the auto-unpacking to work.
Warning: This is a highly experimental demonstration of Rust-like ownership semantics in Julia. It is not intended for production use and should not be depended upon in any project. The borrow checking is performed at runtime, not compile time.
This package demonstrates Rust-like ownership and borrowing semantics in Julia through a macro-based system that performs runtime checks.
Available Macros
Ownership
@own x = value: Create a new owned immutable value@own_mut x = value: Create a new owned mutable value@move new = old: Transfer ownership from one variable to another, invalidating the old variable@take var: Unwrap an owned value to pass ownership to an external function
References and Lifetimes
@lifetime lt begin ... end: Create a scope for references whose lifetimesltare the duration of the block@ref var = value in lt: Create an immutable reference to owned valuevalueand assign it tovarwithin the given lifetime scopelt@ref_mut var = value in lt: Create a mutable reference to owned mutable valuevalueand assign it tovarwithin the given lifetime scopelt
Assignment
@set x = value: Assign a new value to an existing owned mutable variable
Property Access
For owned values and references, property access follows these rules:
- Use
@take xto extract the wrapped value ofx, exiting the BorrowChecker.jl system and allowing direct access to the value.xloses ownership and can’t be used after this. - You can use
getpropertyandsetproperty!normally on owned values and references. Ownership will be transferred when necessary, and errors will be thrown when determined by ownership rules.
Examples
Ownership
First, let’s look at basic ownership.
julia> using BorrowChecker
julia> @own x = 1
Owned{Int64}(1)
This is meant to emulate let x = 42 in Rust.
We can compare it to objects, and the borrow checker will
confirm that we can read it:
julia> x == 1
true
We could also do this by unpacking the value, which moves
ownership:
julia> (@take x) == 1
true
julia> x
[moved]
julia> x == 2
ERROR: Cannot use value: value has been moved
Now, let’s look at a mutable value:
julia> @own_mut y = 1
OwnedMut{Int64}(1)
We change the contents of this variable using @set:
julia> @set y = 2
OwnedMut{Int64}(2)
Note that we can’t do this with immutable values:
julia> @own x = 1;
julia> @set x = 2
ERROR: Cannot assign to immutable
This also works with arrays:
julia> @own array = [1, 2, 3]
Owned{Vector{Int64}}([1, 2, 3])
julia> push!(array, 4)
ERROR: Cannot write to immutable
julia> @own_mut array = [1, 2, 3]
OwnedMut{Vector{Int64}}([1, 2, 3])
julia> push!(array, 4)
OwnedMut{Vector{Int64}}([1, 2, 3, 4])
Just like with immutable values, we can move ownership:
julia> @move array2 = array
Owned{Vector{Int64}}([1, 2, 3, 4])
julia> array
[moved]
julia> array[1] = 5
ERROR: Cannot use value: value has been moved
julia> array2[1] = 5; # works!
Borrowing
References must be created within a @lifetime block. Let’s look at
immutable references first:
julia> @own_mut data = [1, 2, 3];
julia> @lifetime lt begin
@ref ref = data in lt
ref
end
Borrowed{Vector{Int64},OwnedMut{Vector{Int64}}}([1, 2, 3])
Once we have created the reference ref, we are no longer allowed to modify
data until the lifetime lt ends. This helps prevent data races.
After the lifetime ends, we can edit data again:
julia> data[1] = 4; data
OwnedMut{Vector{Int64}}([4, 2, 3])
Note that we can have multiple immutable references at once:
julia> @lifetime lt begin
@ref ref1 = data in lt
@ref ref2 = data in lt
ref1 == ref2
end
true
For mutable references, we can only have one at a time:
julia> @lifetime lt begin
@ref_mut mut_ref = data in lt
@ref_mut mut_ref2 = data in lt
end
ERROR: Cannot create mutable reference: value is already mutably borrowed
And we can’t mix mutable and immutable references:
julia> @lifetime lt begin
@ref ref = data in lt
@ref_mut mut_ref = data in lt
end
ERROR: Cannot create mutable reference: value is immutably borrowed
We can also use references to temporarily borrow values in functions:
julia> function borrow_vector(v::Borrowed) # Signature confirms we only need immutable references
@assert v == [1, 2, 3]
end;
julia> @own vec = [1, 2, 3]
Owned{Vector{Int64}}([1, 2, 3])
julia> @lifetime lt begin
borrow_vector(@ref d = vec in lt) # Immutable borrow
end
julia> vec
Owned{Vector{Int64}}([1, 2, 3])