Yeah seems tricky for sure. One good note is that BorrowChecker.jl by itself already seems to flag captured variables correctly from the existing tracking mechanism.
I’m implementing the borrow checker in SymbolicRegression.jl at the moment: Comparing master...borrow-checker · MilesCranmer/SymbolicRegression.jl · GitHub which results in code that sorta looks like this:
@bind init_hall_of_fame = load_saved_hall_of_fame(@take(saved_state))
@lifetime a begin
@ref a rdatasets = datasets
@ref a roptions = options
@bind for j in 1:nout
@bind :mut hof = strip_metadata(
@take(init_hall_of_fame[j]), roptions, rdatasets[j]
)
@lifetime b begin
@ref b :mut for member in hof.members[hof.exists]
@bind score, result_loss = score_func(
rdatasets[j], @take(member), roptions
)
member.score = @take!(score)
member.loss = @take!(result_loss)
end
end
state.halls_of_fame[j] = @take(hof)
end
end
where functions that are “borrow compatible” are modified like so:
- is_weighted(dataset::Dataset) = !isnothing(dataset.weights)
+ is_weighted(dataset::Borrowed{Dataset}) = !isnothing(dataset.weights)
which is basically equivalent to dataset: &Dataset
in Rust, which prevents dataset
from being modified in that function, but at the same time, let’s you reference the same data from multiple threads.
In doing this, I was pleasantly surprised by an error coming from an accidental capture in a closure! I got the error
Cannot use x: value's lifetime has expired
which turned out to be from something like this:
@bind :mut tasks = Task[]
@lifetime a begin
for i in 1:10
@ref a x = data
push!(tasks, Threads.@spawn f(x)) # bad!
end
end
where x
was borrowed. This means after the @lifetime
scope ended, the access to x
is no longer valid. And it turns out I was doing naughty referencing stuff like this without realising it, and BorrowChecker.jl correctly flagged it!