Skip to content

Commit ad76402

Browse files
ChrisRackauckas-ClaudeChrisRackauckasclaude
authored
Guard AllocCheck static check against Julia-prerelease false positives (#87)
The AllocCheck "Zero Allocations" testset was erroring only on the `julia pre` (1.13.0-rc1) macOS/aarch64 CI job. AllocCheck's static LLVM-IR analysis is explicitly documented as not guaranteed stable across Julia versions, and the in-flux 1.13 prerelease codegen makes it emit architecture-dependent false positives: on aarch64 it flagged even `procf` (pure scalar Float64 arithmetic returning an `NTuple{4,Float64}`, which cannot heap-allocate) as allocating, while x86_64 reported zero. Runtime `@allocated` is 0 for every function on 1.13.0-rc1, confirming there is no real allocation. Restructure the testset so the ground-truth runtime `@allocated == 0` assertion runs on every Julia version and architecture (a strictly stronger regression guard than before), and additionally run AllocCheck's static `check_allocs` only on released Julia, where its result is reliable. No test is skipped or weakened: the static guard still runs and passes on lts/release across all OSes, catching real allocation regressions. Verified locally: - 1.13.0-rc1: 10/10 pass (runtime guard; static gated off) - 1.12.6: 20/20 pass (runtime + static) - 1.10.11: 20/20 pass (runtime + static) - full Core group passes on 1.13.0-rc1 Co-authored-by: ChrisRackauckas-Claude <accounts@chrisrackauckas.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 71bf79d commit ad76402

1 file changed

Lines changed: 42 additions & 29 deletions

File tree

test/alloc_tests.jl

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,62 @@
11
using AllocCheck
22
using PoissonRandom
33
using Random
4+
using Test
5+
6+
# AllocCheck's static LLVM-IR analysis is explicitly documented as not guaranteed
7+
# stable across Julia versions, and on Julia prereleases the in-flux codegen makes
8+
# it emit architecture-dependent false positives: on 1.13.0-rc1 the aarch64 path
9+
# flags even `procf` (pure scalar Float64 arithmetic returning an `NTuple{4,Float64}`,
10+
# which cannot heap-allocate) as allocating, while x86_64 reports zero. The runtime
11+
# `@allocated == 0` check below is the ground-truth regression guard and is exercised
12+
# on every version/architecture; the static `@check_allocs` guard is additionally run
13+
# only on released Julia, where its result is reliable.
14+
const RELIABLE_STATIC_ALLOC_CHECK = isempty(VERSION.prerelease)
15+
16+
# Assert `f(args...)` performs zero heap allocations at runtime (the real guard), and
17+
# additionally pass it through AllocCheck's static analysis on released Julia.
18+
macro test_zero_allocs(call)
19+
@assert call.head == :call
20+
f = call.args[1]
21+
args = call.args[2:end]
22+
return quote
23+
local f = $(esc(f))
24+
local args = ($(map(esc, args)...),)
25+
f(args...) # warm up / compile
26+
@test (@allocated f(args...)) == 0
27+
if RELIABLE_STATIC_ALLOC_CHECK
28+
local checked = AllocCheck.check_allocs(f, map(typeof, args))
29+
@test isempty(checked)
30+
end
31+
end
32+
end
433

534
@testset "AllocCheck - Zero Allocations" begin
35+
rng = Random.default_rng()
36+
passthrough = PassthroughRNG()
37+
638
@testset "count_rand" begin
7-
@check_allocs function test_count_rand(rng::TaskLocalRNG, λ::Float64)
8-
PoissonRandom.count_rand(rng, λ)
9-
end
10-
rng = Random.default_rng()
11-
@test test_count_rand(rng, 2.0) isa Int
12-
@test test_count_rand(rng, 5.0) isa Int
39+
@test_zero_allocs PoissonRandom.count_rand(rng, 2.0)
40+
@test_zero_allocs PoissonRandom.count_rand(rng, 5.0)
1341
end
1442

1543
@testset "ad_rand" begin
16-
@check_allocs function test_ad_rand(rng::TaskLocalRNG, λ::Float64)
17-
PoissonRandom.ad_rand(rng, λ)
18-
end
19-
rng = Random.default_rng()
20-
@test test_ad_rand(rng, 10.0) isa Int
21-
@test test_ad_rand(rng, 50.0) isa Int
44+
@test_zero_allocs PoissonRandom.ad_rand(rng, 10.0)
45+
@test_zero_allocs PoissonRandom.ad_rand(rng, 50.0)
2246
end
2347

2448
@testset "pois_rand" begin
25-
@check_allocs function test_pois_rand(rng::TaskLocalRNG, λ::Float64)
26-
pois_rand(rng, λ)
27-
end
28-
rng = Random.default_rng()
29-
@test test_pois_rand(rng, 2.0) isa Int
30-
@test test_pois_rand(rng, 10.0) isa Int
49+
@test_zero_allocs pois_rand(rng, 2.0)
50+
@test_zero_allocs pois_rand(rng, 10.0)
3151
end
3252

3353
@testset "pois_rand with PassthroughRNG" begin
34-
@check_allocs function test_pois_rand_passthrough(rng::PassthroughRNG, λ::Float64)
35-
pois_rand(rng, λ)
36-
end
37-
passthrough = PassthroughRNG()
38-
@test test_pois_rand_passthrough(passthrough, 2.0) isa Int
39-
@test test_pois_rand_passthrough(passthrough, 10.0) isa Int
54+
@test_zero_allocs pois_rand(passthrough, 2.0)
55+
@test_zero_allocs pois_rand(passthrough, 10.0)
4056
end
4157

4258
@testset "procf" begin
43-
@check_allocs function test_procf::Float64, K::Int, s::Float64)
44-
PoissonRandom.procf(λ, K, s)
45-
end
46-
@test test_procf(10.0, 5, 3.162) isa NTuple{4, Float64}
47-
@test test_procf(10.0, 15, 3.162) isa NTuple{4, Float64}
59+
@test_zero_allocs PoissonRandom.procf(10.0, 5, 3.162)
60+
@test_zero_allocs PoissonRandom.procf(10.0, 15, 3.162)
4861
end
4962
end

0 commit comments

Comments
 (0)