1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
//! `G_DUAL_ZERO_ALLOC` — steady-state heap allocation parity gate.
//!
//! Gate (`RELEASE_BLOCKING`, ADR-0133, math.md §46.3):
//! alloc count during one warm `apply_f` evolution loop at F = Dual<f64>
//! MUST equal the count at F = f64 (both SHOULD be 0 in steady state).
//!
//! # Measurement protocol
//! 1. Build both kernels outside the measurement window.
//! 2. Pre-warm: run one full N-step loop for each type (first call allocates
//! the output Vec; any one-time scratch growth settles here).
//! 3. RESET the counter AFTER the warm-up pass.
//! 4. Measure a SECOND full N-step loop.
//! 5. Assert `dual_count` == `baseline_count`.
//!
//! # Why `apply_f` instead of Evolver?
//! `DiffusionChernoff<Dual<f64>>` does NOT implement `ChernoffFunction<Dual<f64>>`
//! (the trait impl is f64-concrete for SIMD; ADR-0018). The generic path is
//! `DiffusionChernoff::apply_f(tau, &u)`. We iterate `apply_f` in a manual
//! loop, which is exactly what the generic Evolver would do if the trait bound
//! were satisfied.
//!
//! # std requirement
//! `#[global_allocator]` requires `std::alloc::System`.
//! Test binaries link std even when the lib target is `no_std + alloc`.
#![cfg(feature = "slow-tests")]
#![allow(unsafe_code)]
#![allow(clippy::cast_precision_loss)] // usize→f64 for N_STEPS: values ≤ 1000 ≤ 2^52
use std::{
alloc::{GlobalAlloc, Layout, System},
sync::atomic::{AtomicUsize, Ordering},
};
use semiflow::{DiffusionChernoff, Dual, Grid1D, GridFn1D, InterpKind};
// ---------------------------------------------------------------------------
// Counting allocator
// ---------------------------------------------------------------------------
struct CountingAlloc;
static ALLOC_COUNT: AtomicUsize = AtomicUsize::new(0);
unsafe impl GlobalAlloc for CountingAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
System.alloc(layout)
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
System.dealloc(ptr, layout);
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
System.realloc(ptr, layout, new_size)
}
}
#[global_allocator]
static ALLOC: CountingAlloc = CountingAlloc;
fn reset_counter() {
ALLOC_COUNT.store(0, Ordering::SeqCst);
}
fn read_counter() -> usize {
ALLOC_COUNT.load(Ordering::SeqCst)
}
// ---------------------------------------------------------------------------
// Gate constants
// ---------------------------------------------------------------------------
const N_STEPS: usize = 64;
const N_GRID: usize = 128;
const X_MIN: f64 = -10.0;
const X_MAX: f64 = 10.0;
const T_FINAL: f64 = 1.0;
fn a_const_dual(_: Dual<f64>) -> Dual<f64> {
Dual::constant(0.5)
}
fn zero_dual(_: Dual<f64>) -> Dual<f64> {
Dual::constant(0.0)
}
// ---------------------------------------------------------------------------
// f64 baseline — apply_chernoff loop (allocates a fresh Vec per step).
// After warm-up the Vec capacity is stable; we measure a second pass.
// ---------------------------------------------------------------------------
fn run_f64_warmup_then_count() -> usize {
// CubicHermite to match new_generic default (SepticHermite unsupported in apply_f).
let grid = Grid1D::new(X_MIN, X_MAX, N_GRID)
.expect("grid valid")
.with_interp(InterpKind::CubicHermite);
let diff = DiffusionChernoff::with_closure(|_| 0.5_f64, |_| 0.0_f64, |_| 0.0_f64, 0.5, grid);
let u0 = GridFn1D::from_fn(grid, |x: f64| (-x * x).exp());
let tau = T_FINAL / N_STEPS as f64;
let mut u = u0.clone();
// Pre-warm.
for _ in 0..N_STEPS {
u = diff.apply_chernoff(tau, &u).expect("f64 warmup");
}
reset_counter();
// Measurement.
for _ in 0..N_STEPS {
u = diff.apply_chernoff(tau, &u).expect("f64 measure");
}
let _ = u;
read_counter()
}
// ---------------------------------------------------------------------------
// Dual<f64> measurement — apply_f loop, same structure.
// ---------------------------------------------------------------------------
fn run_dual_warmup_then_count() -> usize {
let grid =
Grid1D::<Dual<f64>>::new_generic(Dual::constant(X_MIN), Dual::constant(X_MAX), N_GRID)
.expect("grid valid");
let diff = DiffusionChernoff::<Dual<f64>>::new(
a_const_dual as fn(Dual<f64>) -> Dual<f64>,
zero_dual as fn(Dual<f64>) -> Dual<f64>,
zero_dual as fn(Dual<f64>) -> Dual<f64>,
0.5,
grid,
);
let u0 = GridFn1D::from_fn_generic(grid, |x| Dual::constant((-x.value * x.value).exp()));
let tau = Dual::constant(T_FINAL / N_STEPS as f64);
let mut u = u0.clone();
// Pre-warm.
for _ in 0..N_STEPS {
u = diff.apply_f(tau, &u).expect("dual warmup");
}
reset_counter();
// Measurement.
for _ in 0..N_STEPS {
u = diff.apply_f(tau, &u).expect("dual measure");
}
let _ = u;
read_counter()
}
// ---------------------------------------------------------------------------
// Gate
// ---------------------------------------------------------------------------
/// `G_DUAL_ZERO_ALLOC`: warm `apply_f` alloc count equal at F=`Dual<f64>` vs F=f64.
///
/// Both SHOULD be 0 in steady state (ADR-0041 zero-alloc ping-pong invariant).
/// The load-bearing contract is COUNT equality: a wider element (Dual = 2× bytes)
/// changes first-allocation SIZE, not per-step COUNT. Any positive delta means
/// the dual field leaks heap allocations on the hot path — escalate to Architect.
#[test]
#[ignore = "G_DUAL_ZERO_ALLOC: run with --features slow-tests --release -- --ignored"]
fn g_dual_zero_alloc() {
let baseline = run_f64_warmup_then_count();
let dual_allocs = run_dual_warmup_then_count();
println!(
"G_DUAL_ZERO_ALLOC: f64 baseline={baseline} allocs, \
Dual<f64>={dual_allocs} allocs (gate: dual == baseline)"
);
assert!(
dual_allocs == baseline,
"G_DUAL_ZERO_ALLOC FAIL: Dual<f64> steady-state allocs ({dual_allocs}) \
!= f64 baseline ({baseline}). Dual field leaks per-step heap allocation. \
Escalate to Architect."
);
}