Compile-Time Detection
The @with_pool macro statically analyzes your code at macro expansion time — zero runtime cost, always active.
Escape Analysis (PoolEscapeError)
Detects any tail expression that would deliver a pool-backed array out of the scope. Tracks aliases, containers, and convenience wrappers. Severity is form-based, because the two forms differ in what the macro can know:
- Function form and explicit
return(either form): the value definitively reaches the enclosing function's caller — abeginblock is not a function boundary, soreturninside a block form returns from the enclosing function. These always throwPoolEscapeErrorat expansion. - Block form, implicit last expression: the macro cannot see its own call site, so whether the block's value is used (
out = @with_pool ...) or discarded (a loop body, a bare statement) is undecidable. These emit a@warnand the expansion replaces the escaping value with an inertEscapedPoolArrayguard: discarded → completely silent; used → the first operation throwsEscapedPoolUseErrorwith the variable name, shape, and scope location.
@with_pool pool function bad()
A = acquire!(pool, Float64, 3, 3)
return A # ← PoolEscapeError: function return, always an error
end
out = @with_pool pool begin
v = acquire!(pool, Float64, 100)
w = v # alias tracked
w # ← @warn + guard: `out` is an EscapedPoolArray
end
out[1] # ← EscapedPoolUseError: names `w`, its shape, and the scope
# The guard is inert when the value is never used — this common
# copy-out-and-discard shape runs silently and correctly:
Threads.@threads for k in 1:nmodels
@with_pool pool begin
y = acquire!(pool, Float64, m, n)
compute!(y, k)
results[:, :, k] = y # copy runs; the yielded `y` is guarded; @threads discards it
end
endError messages include the variable name, declaration site, and escaping return expression:
ERROR: LoadError: PoolEscapeError (compile-time)
The following variable escapes the @with_pool scope:
v ← pool-acquired array
Declarations:
[1] v = acquire!(pool, Float64, 100) [myfile.jl:2]
Escaping return:
[1] v [myfile.jl:3]
Fix: Use collect(v) to return owned copies.
Or use a regular Julia array (zeros()/Array{T}()) if it must outlive the pool scope.
in expression starting at myfile.jl:1Incidental Tail Patterns
Beyond the direct-return patterns above, three additional tail shapes are checked — they don't look like a return, but the scope's last expression still evaluates to a pool-backed array, which then escapes as the scope's value:
@with_pool pool begin
v = acquire!(pool, Float64, 100)
acquire!(pool, Float64, 100) # ← direct acquire-call tail
end
@with_pool pool begin
v = acquire!(pool, Float64, 100)
v .= 0.0 # ← broadcast-assign tail (evaluates to `v`)
end
@with_pool pool begin
v = acquire!(pool, Float64, 100)
y = v # ← assignment tail
endFix: end the block with nothing if the value is meant to be discarded — this also silences the warning:
@with_pool pool begin
v = acquire!(pool, Float64, 100)
v .= 0.0
nothing # ← no longer escapes
endIn block form these follow the same warn + EscapedPoolArray-guard treatment as all implicit tails; in function form or under an explicit return, they throw. The escape_lint preference tunes the block-form severity:
"warn"(default) —@warndiagnostic + the guard rewrite described above."error"— block-form implicit tails also throwPoolEscapeError(the strict pre-guard behavior; no guard rewrite needed)."off"— no warning and no guard rewrite (the block tail escapes the raw pool array, as before this feature). Function-form and explicit-returndirect escapes still always error.
using Preferences
Preferences.set_preferences!("AdaptiveArrayPools", "escape_lint" => "error")Mutation Analysis (PoolMutationError)
Structural mutations (resize!, push!, append!, etc.) on pool-backed arrays are rejected:
@with_pool pool begin
v = acquire!(pool, Float64, 100)
push!(v, 1.0) # ← PoolMutationError
endDetected functions: resize!, push!, pop!, pushfirst!, popfirst!, append!, prepend!, insert!, deleteat!, splice!, sizehint!, empty!.
Limitations
Static analysis can't see through opaque function calls:
function sneaky(v)
return v # opaque — macro can't see inside
end
@with_pool pool begin
v = acquire!(pool, Float64, 100)
sneaky(v) # escapes through opaque call — not caught at compile time
endThat's what the runtime layer is for.