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, four 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 (also when wrapped in identity(...)):
@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
end
@with_pool pool begin
(acquire!(pool, Float64, 100), 1) # ← literal holding a direct acquire call
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.
Loops and comprehensions bind elements, not the iterable. Reading an element of a pool-backed array is a plain value, so this is silent and correct:
@with_pool pool begin
order = acquire!(pool, Int, n)
contact = 0
for k in order # `k` is an element (e.g. an Int), not pool memory
k == target && (contact = k)
end
(; contact) # fine — no warning, no guard
endIterating a container literal of pool arrays (for c in (v, w)) is tracked: c is a pool-backed array inside the loop. Iterating a container held in a variable (t = (v, w); for c in t) and destructuring iteration (for (a, b) in ...) are not tracked at compile time. With the default runtime_check = 0, nothing guards those shapes. With runtime_check = 1, the runtime layer inspects the scope's return value at exit and throws PoolRuntimeEscapeError if it still references pool memory — including through a Tuple or NamedTuple — on every supported Julia version.