Skip to content

Commit 8b8d6aa

Browse files
authored
Merge pull request #15 from MilesCranmer/spawn-macro
Create `@spawn` to validate captures to `Threads.@spawn`
2 parents 85a56dc + b52a725 commit 8b8d6aa

File tree

7 files changed

+103
-1
lines changed

7 files changed

+103
-1
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ nested task error: Cannot create mutable reference: `data` is already mutably bo
114114
```
115115

116116
This is because in BorrowChecker.jl's ownership model, similar to Rust, an owned object follows strict borrowing rules to prevent data races and ensure safety.
117+
(Though, in practice, you should use `BorrowChecker.@spawn` instead of `Threads.@spawn`, so that it validates captured variables.)
117118

118119
## Ownership Rules
119120

@@ -155,6 +156,8 @@ In essence: You can have many readers (`Borrowed`) **or** one writer (`BorrowedM
155156
> ```
156157
>
157158
> This will validate that any captured variable is an immutable reference.
159+
> Similarly, you should generally prefer the `BorrowChecker.@spawn` macro instead of
160+
> `Threads.@spawn` to validate captured variables.
158161
159162
## API
160163
@@ -181,6 +184,7 @@ In essence: You can have many readers (`Borrowed`) **or** one writer (`BorrowedM
181184
### Validation
182185
183186
- `@cc closure_expr`: Verifies that closures only capture immutable references.
187+
- `BorrowChecker.@spawn [options...] expr`: A safety wrapper around `Threads.@spawn` that applies `@cc` to the expression (which is internally put inside a closure).
184188
185189
### Loops
186190
@@ -452,6 +456,22 @@ let
452456
end
453457
```
454458

459+
For threads, you can use the `BorrowChecker.@spawn` macro instead of the standard `Threads.@spawn`.
460+
This ensures safe captures by automatically applying `@cc` to the closure (which is generated internally by `@spawn`):
461+
462+
```julia
463+
@own x = 42
464+
@lifetime lt begin
465+
@ref ~lt safe_ref = x
466+
467+
tasks = [
468+
BorrowChecker.@spawn safe_ref + 1
469+
for _ in 1:10
470+
]
471+
sum(fetch, tasks)
472+
end
473+
```
474+
455475
</details>
456476

457477
### Automated Borrowing with `@bc`

docs/src/api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ CurrentModule = BorrowChecker
2727

2828
```@docs
2929
@cc
30+
BorrowChecker.@spawn
3031
```
3132

3233
## Types

src/BorrowChecker.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export @own, @move, @ref, @take, @take!, @lifetime, @clone, @bc, @mut, @cc
2626
using .PreferencesModule: disable_by_default!
2727
using .StaticTraitModule: is_static
2828
using .TypesModule: AsMutable, Lifetime, LazyAccessorOf, is_moved, get_owner, get_symbol, get_immutable_borrows, get_mutable_borrows
29+
using .MacrosModule: @spawn
2930
#! format: on
3031

3132
end

src/macros.jl

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,4 +553,25 @@ _check_capture_allowed(::Type{<:OrLazy{OwnedMut}}) = false
553553
_check_capture_allowed(::Type{<:OrLazy{BorrowedMut}}) = false
554554
# COV_EXCL_STOP
555555

556+
"""
557+
BorrowChecker.@spawn [options...] expr
558+
559+
`Threads.@spawn` but with [`@cc`](@ref) applied to the expression
560+
to ensure safe captures.
561+
"""
562+
macro spawn(expr, args...)
563+
is_borrow_checker_enabled(__module__) ||
564+
return esc(:($(Threads).@spawn($expr, $(args...))))
565+
return esc(_spawn(expr, args...))
566+
end
567+
568+
function _spawn(args...)
569+
inner_closure = gensym("inner_closure")
570+
return quote
571+
let $inner_closure = $(@__MODULE__).@cc () -> $(last(args))
572+
$(Threads).@spawn($(args[1:(end - 1)]...), $(inner_closure)())
573+
end
574+
end
575+
end
576+
556577
end

test/FakeModule/src/FakeModule.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module FakeModule
22

33
using BorrowChecker
4+
using BorrowChecker: @spawn
45
using Test
56

67
function test()
@@ -42,6 +43,12 @@ function test()
4243
@test @bc(f(z)) === f(z)
4344
@test @bc(f(@mut(z_mut))) === f(z_mut)
4445
end
46+
47+
let
48+
# This spawn still works because the borrow checker is disabled
49+
@own x = 1
50+
@test fetch(@spawn x + 1) == 2
51+
end
4552
end
4653

4754
end

test/bc_macro.jl renamed to test/complex_macros.jl

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,3 +728,55 @@ end
728728
)
729729
end
730730
end
731+
732+
@testitem "Simple @spawn macro" begin
733+
using BorrowChecker
734+
using BorrowChecker: @spawn
735+
736+
@test_throws "The closure function captured a variable `x" let
737+
@own x = 1
738+
fetch(@spawn x + 1)
739+
end
740+
@test_throws "The closure function captured" let
741+
@own :mut x = 1
742+
fetch(@spawn x + 1)
743+
end
744+
@test_throws "The closure function captured" let
745+
@own :mut x = 1
746+
@lifetime lt begin
747+
@ref ~lt :mut borrowed = x
748+
fetch(@spawn borrowed + 1)
749+
end
750+
end
751+
let
752+
@own :mut x = 1
753+
@lifetime lt begin
754+
@ref ~lt borrowed = x
755+
@test fetch(@spawn borrowed + 1) == 2
756+
end
757+
end
758+
end
759+
760+
@testitem "Valid uses of @spawn macro" begin
761+
using BorrowChecker
762+
using BorrowChecker: @spawn
763+
764+
let
765+
@own x = [1, 2, 3]
766+
ch = Channel(1)
767+
t = @lifetime lt begin
768+
@ref ~lt borrowed = x
769+
@test fetch(@spawn borrowed .+ 1) == [2, 3, 4]
770+
771+
# We want to verify that the borrow expires in this spawn:
772+
@spawn (take!(ch); borrowed .+ 1)
773+
end
774+
put!(ch, 1)
775+
err_msg = try
776+
fetch(t)
777+
catch e
778+
sprint(showerror, e)
779+
end
780+
@test occursin("Cannot use `borrowed`: value's lifetime has expired", err_msg)
781+
end
782+
end

test/runtests.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ include("ownership_tests.jl")
66
include("reference_tests.jl")
77
include("feature_tests.jl")
88
include("integration_tests.jl")
9-
include("bc_macro.jl")
9+
include("complex_macros.jl")
1010

1111
@testitem "Aqua" begin
1212
using Aqua

0 commit comments

Comments
 (0)