# \[ANN\] BorrowChecker.jl: a borrow checker for Julia

**URL:** https://discourse.julialang.org/t/ann-borrowchecker-jl-a-borrow-checker-for-julia/127897
**Category:** Package Announcements
**Tags:** package
**Created:** [April 9, 2025, 10:49pm UTC](https://discourse.julialang.org/t/ann-borrowchecker-jl-a-borrow-checker-for-julia/127897 "2025-04-09T22:49:12Z")
**Posts on this page:** 20
**Page:** 1

<div class="post-metadata">

### Author: ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)
#### Post date: [April 9, 2025, 10:49pm UTC](https://discourse.julialang.org/t/ann-borrowchecker-jl-a-borrow-checker-for-julia/127897/1 "2025-04-09T22:49:12Z")

</div>

# BorrowChecker.jl

[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://astroautomata.com/BorrowChecker.jl/dev) [![Build Status](https://github.com/MilesCranmer/BorrowChecker.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/MilesCranmer/BorrowChecker.jl/actions/workflows/CI.yml?query=branch%3Amain) [![Coverage](https://coveralls.io/repos/github/MilesCranmer/BorrowChecker.jl/badge.svg?branch=main)](https://coveralls.io/github/MilesCranmer/BorrowChecker.jl?branch=main)

Very happy to finally share BorrowChecker.jl! This is an experimental package for emulating a runtime borrow checker in Julia, using a macro layer over regular code. This is built to mimic Rust’s ownership, lifetime, and borrowing semantics. This tool is mainly to be used in development and testing to flag memory safety issues, and help you design safer code.

## Usage

In Julia, when you write `x = [1, 2, 3]`, the actual _object_ exists completely independently of the variable, and you can refer to it from as many variables as you want without issue:

```julia
x = [1, 2, 3]
y = x
println(length(x))
# 3

```

Once there are no more references to the object, the “garbage collector” will work to free the memory.

Rust works a little differently. For example, the equivalent code is **invalid** in Rust

```rust
let x = vec![1, 2, 3];
let y = x;
println!("{}", x.len());
// error[E0382]: borrow of moved value: `x`

```

Rust refuses to compile this code. Why? Because in Rust, objects (`vec![1, 2, 3]`) are _owned_ by variables. When you write `let y = x`, the ownership of `vec![1, 2, 3]` is _moved_ to `y`. Now `x` is no longer allowed to access it.

To fix this, we would either write

```rust
let y = x.clone();
// OR
let y = &x;

```

to either create a copy of the vector, or _borrow_ `x` using the `&` operator to create a reference. You can create as many references as you want, but there can only be one original object.

This “ownership” paradigm can help improve safety of code. Especially in complex, multithreaded codebases, it is easy to shoot yourself in the foot and modify objects which are “owned” (editable) by something else. This ownership and lifetime model makes it so that you can _prove_ memory safety of code! Standard thread races are literally impossible. (Assuming you are not using `unsafe { ... }` to disable safety features, or the borrow checker itself has a bug, etc.)

In BorrowChecker.jl, we demonstrate an implementation of some of these ideas. The aim is to build a development layer that can help prevent a few classes of memory safety issues, without affecting runtime behavior of code. (I have already found a memory safety issue in my library using this!)

The above example, with BorrowChecker.jl, would look like this:

```julia
using BorrowChecker

@own x = [1, 2, 3]
@own y = x
println(length(x))
# ERROR: Cannot use x: value has been moved

```

You see, the `@own` operation has _bound_ the variable `x` with the object `[1, 2, 3]`. The second operation then moves the object to `y`, and flips the `.moved` flag on `x` so it can no longer be used by regular operations.

The equivalent fixes would respectively be:

```julia
@clone y = x
# OR
@lifetime a begin
    @ref ~a y = x
    #= operations on reference =#
end

```

Note that BorrowChecker.jl does not prevent you from cheating the system and using `y = x`\[1\]. To use this library, you will need to _buy in_ to the system to get the most out of it. But the good news is that you can introduce it in a library gradually: add `@own`, `@move`, etc., inside a single function, and call `@take!` when passing objects to external functions. And for convenience, a variety of standard library functions will automatically forward operations on the underlying objects.

### Example: Preventing Thread Races

BorrowChecker.jl helps prevent data races by enforcing borrowing rules.

Let’s mock up a simple scenario where two threads modify the same array concurrently:

```julia
data = [1, 2, 3]

modify!(x, i) = (sleep(rand()+0.1); push!(x, i))

t1 = Threads.@spawn modify!(data, 4)
t2 = Threads.@spawn modify!(data, 5)

fetch(t1); fetch(t2)

```

This has a silent race condition, and the result will be non-deterministic.

Now, let’s see what happens if we had used BorrowChecker:

```julia
@own :mut data = [1, 2, 3]

t1 = Threads.@spawn @bc modify!(@mut(data), 4)
t2 = Threads.@spawn @bc modify!(@mut(data), 5)

```

Now, when you attempt to fetch the tasks, you will get this error:

```plaintext
nested task error: Cannot create mutable reference: `data` is already mutably borrowed

```

This is because in BorrowChecker.jl’s ownership model, **you can only have one mutable reference to a value at a time**!

The rules are: while you can have multiple _immutable_ references (reading from multiple threads), you cannot have multiple _mutable_ references, and also not both mutable and immutable references. You also cannot access a reference after the lifetime has ended (which in this case, is the duration of the `@bc` macro). You also cannot move ownership while a reference is live.

(The `@lifetime` and `@bc` macros declare the lifetime of references _explicitly_. In Rust, lifetimes are figured out by the compiler. Perhaps one day the [EscapeAnalysis](https://docs.julialang.org/en/v1/devdocs/EscapeAnalysis/) utilities could do something similar in Julia!)

## API

### Basics

- `@own [:mut] x [= value]`: Create a new owned value (mutable if `:mut` is specified)
  - These are `Owned{T}` and `OwnedMut{T}` objects, respectively.
  - You can use `@own [:mut] x` as a shorthand for `@own [:mut] x = x` to create owned values at the start of a function.

- `@move [:mut] new = old`: Transfer ownership from one variable to another (mutable destination if `:mut` is specified). _Note that this is simply a more explicit version of `@own` for moving values._
- `@clone [:mut] new = old`: Create a deep copy of a value without moving the source (mutable destination if `:mut` is specified).
- `@take[!] var`: Unwrap an owned value. Using `@take!` will mark the original as moved, while `@take` will perform a copy.
- `getproperty` and `getindex` on owned/borrowed values return a `LazyAccessor` that preserves ownership/lifetime until the raw value is used.
  - For example, for an object `x::Owned{T}`, the accessor `x.a` would return `LazyAccessor{typeof(x.a), T, Val{:a}, Owned{T}}` which has the same reading/writing constraints as the original.

### References and Lifetimes

- `@bc f(args...; kws...)`: This convenience macro automatically creates a lifetime scope for the duration of the function, and sets up borrowing for any owned input arguments.
  - Use `@mut(arg)` to mark an input as mutable.

- `@lifetime lt begin ... end`: Create a scope for references whose lifetimes `lt` are the duration of the block
- `@ref ~lt [:mut] var = value`: Create a reference, for the duration of `lt`, to owned value `value` and assign it to `var` (mutable if `:mut` is specified)
  - These are `Borrowed{T}` and `BorrowedMut{T}` objects, respectively. Use these in the signature of any function you wish to make compatible with references. In the signature you can use `OrBorrowed{T}` and `OrBorrowedMut{T}` to also allow regular `T`.

### Loops

- `@own [:mut] for var in iter`: Create a loop over an iterable, assigning ownership of each element to `var`. The original `iter` is marked as moved.
- `@ref ~lt [:mut] for var in iter`: Create a loop over an owned iterable, generating references to each element, for the duration of `lt`.

### Disabling BorrowChecker

You can disable BorrowChecker.jl’s functionality by setting `borrow_checker = false` in your LocalPreferences.toml file (using Preferences.jl). When disabled, all macros like `@own`, `@move`, etc., will simply pass through their arguments without any ownership or borrowing checks.

You can also set the _default_ behavior from within a module (make sure to do this at the very top, before any BorrowChecker calls!)

```julia
module MyModule
    using BorrowChecker: disable_by_default!

    disable_by_default!(@ __MODULE__ )
    #= Other code =#
end

```

This can then be overridden by the LocalPreferences.toml file.

If you wanted to use BorrowChecker in a library, the idea is you could disable it by default with this command, but enable it during testing, to flag any problematic memory patterns.

## Further Examples

### Basic Ownership

Let’s look at the basic ownership system. When you create an owned value, it’s immutable by default:

```julia
@own x = [1, 2, 3]
push!(x, 4) # ERROR: Cannot write to immutable

```

For mutable values, use the `:mut` flag:

```julia
@own :mut data = [1, 2, 3]
push!(data, 4) # Works! data is mutable

```

Note that various functions have been overloaded with the write access settings, such as `push!`, `getindex`, etc.

The `@own` macro creates an `Owned{T}` or `OwnedMut{T}` object. Most functions will not be written to accept these, so you can use `@take` (copying) or `@take!` (moving) to extract the owned value:

```julia
# Functions that expect regular Julia types:
push_twice!(x::Vector{Int}) = (push!(x, 4); push!(x, 5); x)

@own x = [1, 2, 3]
@own y = push_twice!(@take!(x)) # Moves ownership of x

push!(x, 4) # ERROR: Cannot use x: value has been moved

```

However, for recursively immutable types (like tuples of integers), `@take!` is smart enough to know that the original can’t change, and thus it won’t mark a moved:

```julia
@own point = (1, 2)
sum1 = write_to_file(@take!(point)) # point is still usable
sum2 = write_to_file(@take!(point)) # Works again!

```

This is the same behavior as in Rust (c.f., the `Copy` trait).

There is also the `@take(...)` macro which never marks the original as moved,  
and performs a `deepcopy` when needed:

```julia
@own :mut data = [1, 2, 3]
@own total = sum_vector(@take(data)) # Creates a copy
push!(data, 4) # Original still usable

```

Note also that for improving safety when using BorrowChecker.jl, the macro will actually store the _symbol_ used.  
This helps catch mistakes like:

```julia
julia> @own x = [1, 2, 3];

julia> y = x; # Unsafe! Should use @clone, @move, or @own

julia> @take(y)
ERROR: Variable `y` holds an object that was reassigned from `x`.

```

This won’t catch all misuses but it can help prevent some.

### Lifetimes

> ****
>
> References let you temporarily _borrow_ values. This is useful for passing values to functions without moving them. These are created within an explicit `@lifetime` block:
> 
> ```julia
> @own :mut data = [1, 2, 3]
> 
> @lifetime lt begin
> @ref ~lt r = data
> @ref ~lt r2 = data # Can create multiple _immutable_ references!
> @test r == [1, 2, 3]
> 
> # While ref exists, data can't be modified:
> data[1] = 4 # ERROR: Cannot write original while immutably borrowed
> end
> 
> # After lifetime ends, we can modify again!
> data[1] = 4
> 
> ```
> 
> Just like in Rust, while you can create multiple _immutable_ references, you can only have one _mutable_ reference at a time:
> 
> ```julia
> @own :mut data = [1, 2, 3]
> 
> @lifetime lt begin
> @ref ~lt :mut r = data
> @ref ~lt :mut r2 = data # ERROR: Cannot create mutable reference: value is already mutably borrowed
> @ref ~lt r2 = data # ERROR: Cannot create immutable reference: value is mutably borrowed
> 
> # Can modify via mutable reference:
> r[1] = 4
> end
> 
> ```
> 
> When you need to pass immutable references of a value to a function, you would modify the signature to accept a `Borrowed{T}` type. This is similar to the `&T` syntax in Rust. And, similarly, `BorrowedMut{T}` is similar to `&mut T`.
> 
> Don’t worry about references being used _after_ the lifetime ends, because the `lt` variable will be expired!
> 
> ```julia
> julia> @own x = 1
> @own :mut cheating = []
> @lifetime lt begin
> @ref ~lt r = x
> push!(cheating, r)
> end
>        
> 
> julia> @show cheating[1]
> ERROR: Cannot use r: value's lifetime has expired
> 
> ```
> 
> This makes the use of references inside threads safe, because the threads _must_ finish inside the scope of the lifetime.
> 
> Though we can’t create multiple mutable references, you _are_ allowed to create multiple mutable references to elements of a collection via the `@ref ~lt for` syntax:
> 
> ```julia
> @own :mut data = [[1], [2], [3]]
> 
> @lifetime lt begin
> @ref ~lt :mut for r in data
> push!(r, 4)
> end
> end
> 
> @show data # [[1, 4], [2, 4], [3, 4]]
> 
> ```

### Mutating Owned Values

> ****
>
> Note that if you have a mutable owned value,  
> you can use `setproperty!` and `setindex!` as normal:
> 
> ```julia
> mutable struct A
> x::Int
> end
> 
> @own :mut a = A(0)
> for _ in 1:10
> a.x += 1
> end
> # Move it to an immutable:
> @own a_imm = a
> 
> ```
> 
> And, as expected:
> 
> ```julia
> julia> a_imm.x += 1
> ERROR: Cannot write to immutable
> 
> julia> a.x += 1
> ERROR: Cannot use a: value has been moved
> 
> ```
> 
> **You should never mutate via variable reassignment.**  
> If needed, you can repeatedly `@own` new objects:
> 
> ```julia
> @own x = 1
> for _ in 1:10
> @own x = x + 1
> end
> 
> ```

### Cloning Values

> ****
>
> Sometimes you want to create a completely independent copy of a value.  
> While you could use `@own new = @take(old)`, the `@clone` macro provides a clearer way to express this intent:
> 
> ```julia
> @own :mut original = [1, 2, 3]
> @clone copy = original # Creates an immutable deep copy
> @clone :mut mut_copy = original # Creates a mutable deep copy
> 
> push!(mut_copy, 4) # Can modify the mutable copy
> @test_throws BorrowRuleError push!(copy, 4) # Can't modify the immutable copy
> push!(original, 5) # Original still usable
> 
> @test original == [1, 2, 3, 5]
> @test copy == [1, 2, 3]
> @test mut_copy == [1, 2, 3, 4]
> 
> ```
> 
> Another macro is `@move`, which is a more explicit version of `@own new = @take!(old)`:
> 
> ```julia
> @own :mut original = [1, 2, 3]
> @move new = original # Creates an immutable deep copy
> 
> @test_throws MovedError push!(original, 4)
> 
> ```
> 
> Note that `@own new = old` will also work as a convenience, but `@move` is more explicit and also asserts that the new value is owned.

### Automated Borrowing with `@bc`

> ****
>
> The `@bc` macro simplifies calls involving owned variables. Instead of manually creating `@lifetime` blocks and references, you just wrap the function call in `@bc`,  
> which will create a lifetime scope for the duration of the function call,  
> and generate references to owned input arguments.  
> Declare which arguments should be mutable with `@mut(...)`.
> 
> ```julia
> @own config = Dict("enabled" => true)
> @own :mut data = [1, 2, 3]
> 
> function process(cfg::OrBorrowed{Dict}, arr::OrBorrowedMut{Vector})
> push!(arr, cfg["enabled"] ? 4 : -1)
> return length(arr)
> end
> 
> @bc process(config, @mut(data)) # => 4
> 
> ```
> 
> Under the hood, `@bc` wraps the function call in a `@lifetime` block, so references end automatically when the call finishes (and thus lose access to the original object).
> 
> This approach works with multiple positional and keyword arguments, and is a convenient  
> way to handle the majority of borrowing patterns. You can freely mix owned, borrowed, and normal Julia values in the same call, and the macro will handle ephemeral references behind the scenes. For cases needing more control or longer lifetimes, manual `@lifetime` usage is a good option.

### Introducing BorrowChecker.jl to Your Codebase

When introducing BorrowChecker.jl to your codebase, the first thing is to `@own` all variables at the top of a particular function. The simplified version of `@own` is particularly useful in this case:

```julia
function process_data(x, y, z)
    @own x, y
    @own :mut z

    #= body =#
end

```

This pattern is useful for generic functions because if you pass an owned variable as either `x`, `y`, or `z`, the original function will get marked as moved.

The next pattern that is useful is to use `OrBorrowed{T}` (basically equal to `Union{T,Borrowed{<:T}}`) and `OrBorrowedMut{T}` aliases for extending signatures). Let’s say you have some function:

```julia
struct Bar{T}
    x::Vector{T}
end

function foo(bar::Bar{T}) where {T}
    sum(bar.x)
end

```

Now, you’d like to modify this so that it can accept _references_ to `Bar` objects from other functions. Since `foo` doesn’t need to mutate `bar`, we can modify this as follows:

```julia
function foo(bar::OrBorrowed{Bar{T}}) where {T}
    sum(bar.x)
end

```

Thus, the full `process_data` function might be something like:

```julia
function process_data(x, y, z)
    @own x, y
    @own :mut z

    @own tasks = [
        Threads.@spawn(@bc(foo(z))),
        Threads.@spawn(@bc(foo(z)))
    ]
    sum(map(fetch, @take!(tasks)))
end

```

Because we modified `foo` to accept `OrBorrowed{Bar{T}}`, we can safely pass immutable references to `z`, and it will _not_ be marked as moved in the original context! Immutable references are safe to pass in a multi-threaded context, so this doubles as a good way to prevent unintended thread races.

Note that this will nicely handle the case of multiple mutable references—if we had written `@bc foo(@mut(z))` while the other thread was running, we would see a `BorrowRuleError` because `z` is already mutably borrowed!

* * *

Anyways, interested to hear people’s thoughts. I’ve dogfooded the library a bit in SymbolicRegression.jl (where it helped me discover a new data race!) but please let me know if you get to try it, and any suggestions to improve the usability. It’s still a bit of an experiment!

* * *

1. Luckily, the library has a way to try flag such mistakes by recording symbols used in the macro.

---

<div class="post-metadata">

### Author: ![ablaom](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ablaom/32/4889_2.png) [@ablaom](https://discourse.julialang.org/u/ablaom)
#### Post date: [April 10, 2025, 4:35am UTC](https://discourse.julialang.org/t/ann-borrowchecker-jl-a-borrow-checker-for-julia/127897/2 "2025-04-10T04:35:11Z")

</div>

Very cool.

---

<div class="post-metadata">

### Author: ![digital\_carver](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/digital_carver/32/33818_2.png) [@digital\_carver](https://discourse.julialang.org/u/digital_carver)
#### Post date: [April 10, 2025, 1:26pm UTC](https://discourse.julialang.org/t/ann-borrowchecker-jl-a-borrow-checker-for-julia/127897/3 "2025-04-10T13:26:22Z")

</div>

It’s understandable why it’s opt-in at this point, but that also takes away from the utility of something like this a little bit, since the point of Rust’s safety features is to save us from ourselves. A good compromise to me seems like to have a linting tool that tells you when you’ve forgotten the BorrowChecker annotations (that’s customizable and e.g. easy to add exceptions to).

From a bit of searching, [GitHub - RelationalAI-oss/ReLint.jl: Linter for the Julia programming language](https://github.com/RelationalAI-oss/ReLint.jl) seems like an existing Julia linter that allows adding custom rules. But maybe JuliaSyntax.jl makes it so that creating a small custom linter for this package is not especially difficult anyway - I’m not sure.

Does having linter support for this seem feasible and useful to you? Have you had any ideas in this direction already?

---

<div class="post-metadata">

### Author: ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)
#### Post date: [April 10, 2025, 1:52pm UTC](https://discourse.julialang.org/t/ann-borrowchecker-jl-a-borrow-checker-for-julia/127897/4 "2025-04-10T13:52:39Z")

</div>

> [@digital\_carver](#):
>
> It’s understandable why it’s opt-in at this point, but that also takes away from the utility of something like this a little bit, since the point of Rust’s safety features is to save us from ourselves.

I would just add that—in my experience adding it to SymbolicRegression.jl—I really like that I can progressively cover more of the library with BorrowChecker.jl. I guess it’s kind of also in line with Julia style: easy to get started (and often that can be good enough), and then gets progressively more complex the more optimizations you want. BorrowChecker.jl similarly operates as a layer on top of regular Julia code. In fact if you turn it off in the preferences, the macros just evaporate. So `@own x = [1, 2, 3]` just becomes `x = [1, 2, 3]`.

If it was “all or nothing,” or “opt-out,” I worry it might make integration hard (depending on what you mean by opt-in). A lot of common coding patterns are simply incompatible with a borrow checker (often because they are inherently unsafe, as I’m sure you know) and so you end up needing to restructure a lot of code. It’s much easier to start at a single function and gradually expand from there, like:

```julia
function my_internal_function(x, y)
    @own :mut x, y # set these variables up with borrowchecker
    @bc foo!(@mut(x)) # pass mutable reference to x, rather than x itself
    #= ^would need ::OrBorrowed{T} signature in foo! =#

    return process(@take!(x), @take!(y))
end

```

These `@take!` result in a move operation, meaning that `x` and `y` here would no longer be accessible. It unwraps them, so that `process` could be left as-is. And the `@bc` call ensures that the references expire when the function returns, so there wouldn’t be any leftover pointers.

Just to note there are also a couple features to help catch “cheating”, like this one:

```julia-repl
julia> @own x = [1, 2, 3]
Owned{Vector{Int64}}([1, 2, 3], :x)

julia> y = x
Owned{Vector{Int64}}([1, 2, 3], :x)

julia> @take(y)
ERROR: Variable `y` holds an object that was reassigned from `x`.

```

which helps ensure you don’t accidentally step around the borrow checker. This symbol awareness is also part of why I am using a macro-heavy API.

> [@digital\_carver](#):
>
> Does having linter support for this seem feasible and useful to you? Have you had any ideas in this direction already?

I don’t think it’s possible. I think the language itself would need to be redesigned for this sort of thing to be possible because there is too little metadata available statically in the language itself (one of the tradeoffs for a dynamic language). I think it really needs to be a dynamic borrow checker and have full control of the runtime as is done currently.

Note that [EscapeAnalysis · The Julia Language](https://docs.julialang.org/en/v1/devdocs/EscapeAnalysis/) operates at the SSA IR level, rather than Julia syntax. And even that seems pretty hard to do correctly.

---

<div class="post-metadata">

### Author: ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)
#### Post date: [April 10, 2025, 2:00pm UTC](https://discourse.julialang.org/t/ann-borrowchecker-jl-a-borrow-checker-for-julia/127897/5 "2025-04-10T14:00:55Z")

</div>

Another important thing to note is that there isn’t yet really a concept of “immutable reference” to an arbitrary object in Julia. The closest thing is `Base.Experimental.Const`, but this is only for arrays, and it doesn’t prevent you from editing the underlying array:

```julia-repl
julia> x = [1, 2, 3]
3-element Vector{Int64}:
 1
 2
 3

julia> z = Base.Experimental.Const(x);

julia> x[2] = 5
5

julia> z
3-element Base.Experimental.Const{Int64, 1}:
 1
 5
 3

```

BorrowChecker.jl’s ownership model is designed to prevent this type of thing:

```julia-repl
julia> @own :mut x = [1, 2, 3]
OwnedMut{Vector{Int64}}([1, 2, 3], :x)

julia> lt = BorrowChecker.Lifetime() #= internal call; use `@lifetime` instead! =#
BorrowChecker.TypesModule.Lifetime(Any[], Base.Threads.SpinLock(0), Any[], Base.Threads.SpinLock(0), false)

julia> @ref ~lt r = x
Borrowed{Vector{Int64},OwnedMut{Vector{Int64}}}([1, 2, 3], :r)

julia> x[2] = 5
ERROR: Cannot write `x` while immutably borrowed

```

---

<div class="post-metadata">

### Author: ![Mason](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mason/32/2423_2.png) [@Mason](https://discourse.julialang.org/u/Mason)
#### Post date: [April 10, 2025, 2:58pm UTC](https://discourse.julialang.org/t/ann-borrowchecker-jl-a-borrow-checker-for-julia/127897/6 "2025-04-10T14:58:28Z")

</div>

Very cool work @MilesCranmer! One thing I think that would be interesting to explore is trying to get JET.jl to be able to statically detect the errors thrown by BorrowChecker.jl

For instance, it correctly detects this error:

```julia-repl
julia> using BorrowChecker, JET

julia> report_call() do
           @own x = [1, 2, 3]
           push!(x, 4)
       end
═════ 1 possible error found ═════
┌ (::var"#7#8")() @ Main ./REPL[11]:3
│┌ push!(r::Owned{Vector{Int64}}, items::Int64) @ BorrowChecker.OverloadsModule /home/masonp/.julia/packages/BorrowChecker/hz5T3/src/overloads.jl:139
││┌ request_value(r::Owned{Vector{Int64}}, ::Val{:write}) @ BorrowChecker.SemanticsModule /home/masonp/.julia/packages/BorrowChecker/hz5T3/src/semantics.jl:90
│││┌ validate_mode(r::Owned{Vector{Int64}}, ::Val{:write}) @ BorrowChecker.SemanticsModule /home/masonp/.julia/packages/BorrowChecker/hz5T3/src/semantics.jl:56
││││ may throw either of: BorrowChecker.SemanticsModule.throw(BorrowChecker.SemanticsModule.MovedError(BorrowChecker.SemanticsModule.get_symbol(r::Owned{Vector{Int64}})::Symbol)::MovedError), BorrowChecker.SemanticsModule.throw(BorrowChecker.SemanticsModule.BorrowRuleError(string("Cannot write to ", var_str::String)::String)::BorrowRuleError)
│││└────────────────────

julia> report_call() do
           @own :mut x = [1, 2, 3]
           push!(x, 4)
       end
No errors detected

```

but not this one:

```julia-repl
julia> report_call() do
           @own x = [1, 2, 3]
           @own y = x
           length(x)
       end
No errors detected

```

I wonder if there’s something about the way the error is thrown here that makes it hard for JET.jl to detect.

* * *

The other thing I’d like to mention is that I think this could be very helpful for making Bumper.jl safe. If we could combine BorrowChecker lifetimes with Bumper allocators, that might be a major safety win.

---

<div class="post-metadata">

### Author: ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)
#### Post date: [April 10, 2025, 3:20pm UTC](https://discourse.julialang.org/t/ann-borrowchecker-jl-a-borrow-checker-for-julia/127897/7 "2025-04-10T15:20:31Z")

</div>

Very cool!! Excited to hear how it goes with Bumper.

Regarding the example, my guess is that JET can detect the first one because it is accessible in the type domain. `@own x = [1, 2, 3]` creates an `Owned{Vector{Int64}}` which hits the trait

```julia
is_mutable(::Owned) = false

```

Therefore it will refuse any requests to unwrap the stored data if a `:write` operation is requested, such as `push!`.

More technical details:

- The `push!` function calls `request_value(r, Val(:write))` [here](https://github.com/MilesCranmer/BorrowChecker.jl/blob/babda8a3c9f3e1f7adc880db1d0389eb312948b8/src/overloads.jl#L161-L163) to unwrap the value before forwarding it to the underlying `push!` call. The overloaded function uses this `request_value` function to basically state “I am going to modify this value” or “I am only going to read this value”.
  - So far, I’ve added all of these by hand, in the [overloads.jl file](https://github.com/MilesCranmer/BorrowChecker.jl/blob/d2a42946b4dc5dbdaeb48e70f399ac3011effa2d/src/overloads.jl).
    - _(Maybe in the future it could be possible to automate this? Unfortunately the `!` suffix isn’t a reliable indicator and can’t indicate multiple arguments. But maybe there’s a way to figure it out from the IR or something…)_

- The `request_value` function then calls `validate_mode(r, Val(:write))` [here](https://github.com/MilesCranmer/BorrowChecker.jl/blob/babda8a3c9f3e1f7adc880db1d0389eb312948b8/src/semantics.jl#L56-L71) which performs the `!is_mutable(r) && mode == :write` check, which is what triggers this error.

I think it is possible to evaluate this entirely in the type domain, which is why JET.jl seems to flag it.

However, a move operation is indicated in the value, not the type, [here](https://github.com/MilesCranmer/BorrowChecker.jl/blob/d2a42946b4dc5dbdaeb48e70f399ac3011effa2d/src/types.jl#L90):

```julia
mutable struct OwnedMut{T} <: AbstractOwned{T}
    const value::T
    @atomic moved::Bool
    @atomic immutable_borrows::Int
    @atomic mutable_borrows::Int
    const symbol::Symbol
end

```

Basically when a move happens, it just does `.moved = true`. Then all requests to `request_value` are denied.

In other words, I guess JET can’t detect the second error because the `moved` is in value space.

I don’t suppose there’s any way around this?

---

<div class="post-metadata">

### Author: ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)
#### Post date: [April 10, 2025, 3:42pm UTC](https://discourse.julialang.org/t/ann-borrowchecker-jl-a-borrow-checker-for-julia/127897/8 "2025-04-10T15:42:49Z")

</div>

P.S., one thing that would be cool, and I have no idea if this is possible… is to make all generated owned and borrowed objects _subtypes_ of the original object type. Obviously this couldn’t be done with a single struct because Julia doesn’t have multiple inheritance, but I wonder if structs could be generated on the fly? (And then dispatched to internal methods using traits)

Like, for

```julia
@own x = [1, 2, 3]

```

Is there a way to automatically generate some `var"#OwnedVectorInt#" <: Vector{Int}` struct (or subtyped whatever internal abstract type is used for multiple dispatch) that is equivalent in contents to the current `Owned{Vector{Int}}`, but at the same time, would hit the `push!(x::Owned, i)` method _before_ the `push!(x::Vector, i)` one?

(Maybe this is a job for Cassette.jl… but sadly that library isn’t working anymore)

If we could get that working, then in principle, you wouldn’t necessarily need to rewrite internal signatures like

```diff
- function foo(x::MyType)
+ function foo(x::OrBorrowed{MyType})

```

whenever you want to allow references to be passed around.

---

<div class="post-metadata">

### Author: ![Xiaodong\_Hu](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/xiaodong_hu/32/46382_2.png) [@Xiaodong\_Hu](https://discourse.julialang.org/u/Xiaodong_Hu)
#### Post date: [April 11, 2025, 12:21am UTC](https://discourse.julialang.org/t/ann-borrowchecker-jl-a-borrow-checker-for-julia/127897/9 "2025-04-11T00:21:47Z")

</div>

I really like this package and want to widely used it in my research. In scientific computation, memory-using efficiency can be an important bottleneck for performance. And julia’s GC is not satisfied when intense multithreading/distribution is used.

I just wonder if the object is indeed dropped _immediately_ once the lifetime ends or the ownership is moved, as C++/Rust’s RAII memory management? Or it is just a syntax-level constraint, while the memory management is still controlled by julia’s GC?

I also wonder if this rust-like borrow-checker is able to pass more information to the compiler for more aggressive optimization?

---

<div class="post-metadata">

### Author: ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)
#### Post date: [April 11, 2025, 12:46am UTC](https://discourse.julialang.org/t/ann-borrowchecker-jl-a-borrow-checker-for-julia/127897/10 "2025-04-11T00:46:58Z")

</div>

Thanks for your interest!

> [@Xiaodong\_Hu](#):
>
> I just wonder if the object is indeed dropped _immediately_ once the lifetime ends or the ownership is moved, as C++/Rust’s RAII memory management? Or it is just a syntax-level constraint, while the memory management is still controlled by julia’s GC?

Great question. First, I should say that a “move” doesn’t copy memory, it’s just a regular assignment operation. So when ownership is “moved”, it’s basically just passing a pointer to the same underlying object in memory. This is no different than regular Julia, only there’s this `.moved` attribute that prevents any further operations on that object using the original variable (that it was moved out of).

But, right now, no, BorrowChecker does not involve any memory management. It only does the “checking” part for mutability/lifetimes/ownerships. The `@lifetime` and `@bc` operations clean up the references at the end of their scope, so they could instantly finalize those variables, but they are just references, so it wouldn’t be saving much.

To get Rust-like memory cleanup without a GC, I think Julia would need to further develop [EscapeAnalysis · The Julia Language](https://docs.julialang.org/en/v1/devdocs/EscapeAnalysis/). But this would be independent of BorrowChecker (though maybe one day the package could use it for automatic moves or something).

In the short term I would recommend checking out @Mason’s Bumper.jl, which lets you do bump allocation in Julia (no GC needed!). What Mason was discussing above was whether BorrowChecker could somehow be used with Bumper to cleanly prevent people from using objects leaked from the bump allocation used inside a `@no_escape` block.

I guess another thought about where BorrowChecker can help in terms of memory is it lets you be safer about sharing memory between threads. So rather than needing to pre-emptively `deepcopy` everything to avoid race conditions, you can safely share references to the same underlying memory, because the ownership model prevents any editing of that memory while those references are active.

---

<div class="post-metadata">

### Author: ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)
#### Post date: [April 11, 2025, 12:56am UTC](https://discourse.julialang.org/t/ann-borrowchecker-jl-a-borrow-checker-for-julia/127897/11 "2025-04-11T00:56:00Z")

</div>

P.S., made a library logo!

 ![image](https://global.discourse-cdn.com/julialang/original/3X/1/d/1d17e76c4dda68d383c222b7c31575bb1ef83573.jpeg)

It’s “[Ferris the Crab](https://rustacean.net/)” juggling the Julia dots 😃

---

<div class="post-metadata">

### Author: ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)
#### Post date: [April 11, 2025, 10:00pm UTC](https://discourse.julialang.org/t/ann-borrowchecker-jl-a-borrow-checker-for-julia/127897/12 "2025-04-11T22:00:42Z")

</div>

Quick update—there is a decent number of collection operations now, which means the aliasing detection is pretty useful! Check this out:

```julia-repl
julia> @own x = Set([[1], [2]]);

julia> @own y = Set([[3]]);

julia> union(x, y)
ERROR: Refusing to return result of union, as the
output of type `Set{Vector{Int64}}` contains mutable
elements. This can result in unintended aliasing
with the original array, so you must first call
`@take!(...)` on each input argument when calling
this function.

```

This kind of thing can prevent some _ **really** _ subtle aliasing bugs:

```julia-repl
julia> x = Set([[1], [2]]);

julia> y = Set([[3]]);

julia> z = union(x, y)
Set{Vector{Int64}} with 3 elements:
  [3]
  [1]
  [2]

julia> pop!(x)[] = 3 #= this also edits z!! =#
3

julia> z
Set{Vector{Int64}} with 3 elements:
  [3]
  [3]
  [2]

```

---

<div class="post-metadata">

### Author: ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)
#### Post date: [April 11, 2025, 10:58pm UTC](https://discourse.julialang.org/t/ann-borrowchecker-jl-a-borrow-checker-for-julia/127897/13 "2025-04-11T22:58:46Z")

</div>

I thought it was pretty obvious, but given the questions that showed up so soon, it might be worth it for the introductory paragraph to clarify to passing readers that this does not modify Julia’s implementation to become closer to Rust’s, let alone any desirable performance aspects. There already is a language with Rust’s semantics that also uses LLVM.

---

<div class="post-metadata">

### Author: ![minetest2048](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/minetest2048/32/45961_2.png) [@minetest2048](https://discourse.julialang.org/u/minetest2048)
#### Post date: [April 12, 2025, 12:16am UTC](https://discourse.julialang.org/t/ann-borrowchecker-jl-a-borrow-checker-for-julia/127897/14 "2025-04-12T00:16:26Z")

</div>

As much as I like this, I think this logo is better for a Rust - Julia interop package

---

<div class="post-metadata">

### Author: ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)
#### Post date: [April 12, 2025, 12:49am UTC](https://discourse.julialang.org/t/ann-borrowchecker-jl-a-borrow-checker-for-julia/127897/15 "2025-04-12T00:49:08Z")

</div>

![IMG_3310](https://global.discourse-cdn.com/julialang/original/3X/c/9/c95786d61581dd5e25bba23ede149b0fd9f05bd1.jpeg)

The README makes this clear, no? Or maybe you are suggesting I should copy this part to the discourse?

(I think I deleted it when moving the text because discourse didn’t have the same warning syntax as GitHub markdown)

---

<div class="post-metadata">

### Author: ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)
#### Post date: [April 12, 2025, 1:06am UTC](https://discourse.julialang.org/t/ann-borrowchecker-jl-a-borrow-checker-for-julia/127897/16 "2025-04-12T01:06:38Z")

</div>

Hm, I’m not sure. I would think the Rust logo itself would be more appropriate for that?

 ![IMG_3313](https://global.discourse-cdn.com/julialang/original/3X/a/3/a3847462284cc81bb80e62eceaea930dfd9e8af9.png)

The Ferris crab I feel is better suited to imply a softer association, i.e., “Rust-like” or “Rust-associated” things. Similar to having the Julia dots, compared with the full Julia logo.

As to why it is Rust associated: all the ownership, lifetime, and borrow rules here are taken directly from Rust itself (I even check what the Rust linter says for translated examples, to check I’ve got the rules correct). I think Rust is kind of so semantically associated with its borrow checker (see what comes up when googling “borrow checker”), it just feels more than appropriate.

On a more practical note I don’t know how to illustrate a borrow checker in a non-technical but intuitive way 😓 I think just having the Rust-yness implied is the easiest association?

---

<div class="post-metadata">

### Author: ![cjdoris](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cjdoris/32/213133_2.png) [@cjdoris](https://discourse.julialang.org/u/cjdoris)
#### Post date: [April 12, 2025, 12:41pm UTC](https://discourse.julialang.org/t/ann-borrowchecker-jl-a-borrow-checker-for-julia/127897/17 "2025-04-12T12:41:20Z")

</div>

This is a cool package!

There are two ways you could write a package like this, one is to do all the checking at run-time, and the other is to have a single macro that rewrites the given code to have these stricter ownership semantics. The latter should compile to very fast code. The former is how you do it. I’m curious if you thought about doing it the other way and what the tradeoffs might be?

---

<div class="post-metadata">

### Author: ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)
#### Post date: [April 12, 2025, 1:05pm UTC](https://discourse.julialang.org/t/ann-borrowchecker-jl-a-borrow-checker-for-julia/127897/18 "2025-04-12T13:05:45Z")

</div>

Thanks!

I’m not sure I understand, can you say more about the second idea?

---

<div class="post-metadata">

### Author: ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)
#### Post date: [April 12, 2025, 6:38pm UTC](https://discourse.julialang.org/t/ann-borrowchecker-jl-a-borrow-checker-for-julia/127897/19 "2025-04-12T18:38:37Z")

</div>

> [@cjdoris](#):
>
> a single macro that rewrites the given code to have these stricter ownership semantics. The latter should compile to very fast code.

Sounds like that would require implementing a Rust-like compiler, only with worse-than-`unsafe` code from everywhere else that doesn’t use `BorrowChecker`. If writing with Rust semantics sometimes is desirable, there’d be more ecosystem and performance gains from Rust-in-Julia interop.

---

<div class="post-metadata">

### Author: ![cjdoris](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cjdoris/32/213133_2.png) [@cjdoris](https://discourse.julialang.org/u/cjdoris)
#### Post date: [April 12, 2025, 7:50pm UTC](https://discourse.julialang.org/t/ann-borrowchecker-jl-a-borrow-checker-for-julia/127897/20 "2025-04-12T19:50:10Z")

</div>

> [@MilesCranmer](#):
>
> I’m not sure I understand, can you say more about the second idea?

For example we could have

```julia
@rusty begin
    x = [1, 2, 3]
    y = x
    length(x)
end

```

transform to something like

```julia
begin
    x = Owned([1, 2, 3])
    y = Owned(rusty_take(x))
    x = Moved()
    rusty_call(length, x)
end

```

where the final call will fail because one of its args has moved. Unlike BorrowChecker.jl, the `Owned` type just stores the value and nothing else.

I’m hoping (without having thought about it at all deeply) that you can do this with a purely syntactic transformation, plus some new types and functions.

Just a thought… and not trying to criticise BorrowChecker.jl at all, just interested.

[Next page](https://discourse.julialang.org/t/ann-borrowchecker-jl-a-borrow-checker-for-julia/127897.md?page=2)
