Skip to main content

shape_jit/ffi/
arc.rs

1//! ARC reference counting FFI for JIT-compiled code.
2//!
3//! ## Route A close (ADR-006 §2.7.14 / W11-jit-new-array)
4//!
5//! Both entry points operate on a JIT-emitted `UnifiedValue<T>` allocation
6//! pointer (or, equivalently, a v2 `*mut HeapHeader`-prefixed allocation —
7//! BOTH layouts carry a `kind: u16` at offset 0 and a refcount at a
8//! layout-fixed offset). The caller contract (post-W11) is that the MIR
9//! emitter only emits these calls for slots whose `NativeKind` satisfies
10//! [`NativeKind::is_refcounted`] — `String` (Arc<String> raw pointer) or
11//! `Ptr(HeapKind::*)` (Arc<HeapValue> raw pointer wrapped in
12//! `UnifiedValue`). Raw scalar slots (`Int64`, `Float64`, `Bool`, etc.)
13//! never reach this FFI; the discrimination lives at the emitter side
14//! in [`crate::mir_compiler::ownership::refcount_disposition`].
15//!
16//! ## Refcount layout
17//!
18//! Two `#[repr(C)]` shapes flow through this FFI today:
19//!
20//! 1. **JIT-emitted `UnifiedValue<T>`** (`ffi/jit_kinds.rs`):
21//!    ```text
22//!    offset  0: kind: u16
23//!    offset  2: flags: u8
24//!    offset  3: _reserved: u8
25//!    offset  4: refcount: AtomicU32  ← retain/release target
26//!    offset  8: data: T
27//!    ```
28//!    Used by `box_string`, `box_typed_object`, `unified_box`-family.
29//!
30//! 2. **v2 `HeapHeader`-prefixed allocations** (`shape_value::v2::heap_header`):
31//!    ```text
32//!    offset  0: refcount: AtomicU32  ← retain/release target
33//!    offset  4: kind: u16
34//!    offset  6: flags: u8
35//!    offset  7: _pad: u8
36//!    ```
37//!    Used by `TypedArray<T>`, `TypedClosureHeader`, `v2_alloc_struct`.
38//!    These have their OWN dedicated FFI (`jit_v2_retain` / `jit_v2_release`)
39//!    and the MIR emitter routes them via `Skip_TypedCellCarrier`
40//!    (`v2_typed_array_elem_kind`-guarded), so they do NOT reach this
41//!    function in production. Disambiguating the two shapes from
42//!    `ptr` alone would require a tag-bit probe (CLAUDE.md "Forbidden
43//!    Patterns" #4); the contract is "MIR emitter routes by kind".
44//!
45//! ## Discriminator on release
46//!
47//! When refcount reaches zero, the underlying `Box::from_raw` reclaim
48//! needs to know the inner `T` of the `UnifiedValue<T>` (or the
49//! `HeapValue` discriminant for the `Ptr(HeapKind::*)` arms). The
50//! `kind: u16` field at offset 0 of `UnifiedValue` IS the canonical
51//! discriminator (§2.7.6 / Q8 single-discriminator — same field, same
52//! semantics as `HeapValue::kind()`); reading it is NOT a tag-bit probe
53//! because it's a structural field on the heap object, not a bit-pack
54//! on an inline value. The free path dispatches on this `kind` via
55//! [`shape_value::release::release_v2_heap_by_kind`].
56//!
57//! ## Forbidden
58//!
59//! - Bool-default fallback for unknown kind (CLAUDE.md "Forbidden rationalizations").
60//! - `tag_bits` decode on the `ptr` value (CLAUDE.md "Forbidden Patterns" #4).
61//! - Silent no-op'ing the body (the supervisor explicitly refused this
62//!   shape during the W11 reopen — "Soft-fail counter for now" pattern).
63//! - "ARC bridge" / "retain helper" / "kind-injection adapter" framing
64//!   (CLAUDE.md "Renames to refuse on sight" — broader family rule).
65
66use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
67
68/// Refcount call counters for W11-jit-new-array leak-balance verification.
69/// Surfaced via `--trace-jit=shape_jit::arc_counters=info` (cluster-2
70/// closure-wave-F tracing-crate migration 2026-05-16; supersedes
71/// `SHAPE_JIT_ARC_COUNTERS=1`). The atomic writes still happen
72/// unconditionally (the counters are pub(crate) accessible from tests); the
73/// tracing macros at the reader site collapse to no-ops when the `jit-trace`
74/// feature is OFF, so the read-of-counters work is skipped in release
75/// builds. Per the supervisor's reopen Step 4: in addition to stdout
76/// matching, confirm the retain/release sequence is balanced via these
77/// counters.
78pub(crate) static JIT_ARC_RETAIN_CALLS: AtomicU64 = AtomicU64::new(0);
79pub(crate) static JIT_ARC_RELEASE_CALLS: AtomicU64 = AtomicU64::new(0);
80pub(crate) static JIT_ARC_RELEASE_FREES: AtomicU64 = AtomicU64::new(0);
81
82/// String-carrier-specific counters for the cluster-2-closure-wave-E
83/// measurement protocol (cluster-2-inventory §F). These track the §2.7.5
84/// `Arc::into_raw(Arc<String>) as u64` carrier path independently from the
85/// `UnifiedValue<T>` counters above — `arc_string_constant` allocates an
86/// `Arc<String>` and bumps the strong count to 2 (the "permanent share"
87/// + "active share" discipline at `ffi/string.rs:111-143`), while the
88/// JIT-emitted retain/release pairs operate on the active share via
89/// `jit_arc_string_retain` / `jit_arc_string_release`.
90///
91/// The leak shape per the inventory §F.3 estimate is one permanent share
92/// per distinct `arc_string_constant` allocation never released; these
93/// counters quantify it directly:
94///
95/// - `STRING_CONSTANT_ALLOCS` = number of `arc_string_constant` calls
96///   (one per JIT-compile-time `MirConstant::Str` / `MirConstant::StringId`
97///   / `MirConstant::Method` materialization)
98/// - `STRING_RETAIN_CALLS` = `jit_arc_string_retain` invocations (per
99///   runtime active-share copy)
100/// - `STRING_RELEASE_CALLS` = `jit_arc_string_release` invocations (per
101///   runtime active-share drop)
102/// - `STRING_RELEASE_FREES` = times the release reached refcount 0 (the
103///   §2.7.5 `Arc<String>` was actually deallocated). With the permanent-
104///   share discipline this should equal 0 for every constant produced by
105///   `arc_string_constant` (the leak surface).
106///
107/// Leak quantification: `STRING_CONSTANT_ALLOCS - STRING_RELEASE_FREES` =
108/// number of permanently-leaked `Arc<String>` allocations at program end.
109pub(crate) static STRING_CONSTANT_ALLOCS: AtomicU64 = AtomicU64::new(0);
110pub(crate) static STRING_RETAIN_CALLS: AtomicU64 = AtomicU64::new(0);
111pub(crate) static STRING_RELEASE_CALLS: AtomicU64 = AtomicU64::new(0);
112pub(crate) static STRING_RELEASE_FREES: AtomicU64 = AtomicU64::new(0);
113
114/// Offset of the `refcount: AtomicU32` field within a JIT-emitted
115/// `UnifiedValue<T>` allocation. Must match `#[repr(C)]` layout of
116/// `crate::ffi::jit_kinds::UnifiedValue`.
117const UNIFIED_VALUE_REFCOUNT_OFFSET: usize = 4;
118
119/// Retain a JIT-emitted refcounted heap value.
120///
121/// `ptr` is a non-null pointer to a `UnifiedValue<T>` allocation produced
122/// by `unified_box` / `box_string` / `box_typed_object`. Atomically bumps
123/// the `refcount: AtomicU32` field at offset 4 by 1 (Relaxed ordering,
124/// matching the typed-Arc retain contract).
125///
126/// Caller contract (W11-jit-new-array): the MIR emitter only emits this
127/// call for slots whose `NativeKind` satisfies
128/// `NativeKind::is_refcounted` (i.e. `String` or `Ptr(HeapKind::*)`).
129/// Scalar slots are filtered out at the emitter side per ADR-006 §2.7.5
130/// stamp-at-compile-time.
131///
132/// # Safety
133///
134/// `ptr` must be a non-null `*const UnifiedValue<T>` from a live
135/// JIT-emitted heap allocation, or null. Passing a dangling, mistyped,
136/// or non-`UnifiedValue` pointer is undefined behavior. Null is
137/// silently no-op'd (the MIR emitter MAY route a null-initialized slot
138/// here on a dead-store path that the borrow-checker proves never
139/// reads).
140#[unsafe(no_mangle)]
141pub extern "C" fn jit_arc_retain(ptr: *const u8) {
142    if ptr.is_null() {
143        return;
144    }
145    JIT_ARC_RETAIN_CALLS.fetch_add(1, Ordering::Relaxed);
146    // SAFETY: see fn docs. ptr is a *const UnifiedValue<_> with a
147    // valid AtomicU32 at offset 4 per the #[repr(C)] layout.
148    unsafe {
149        let refcount_ptr = ptr.add(UNIFIED_VALUE_REFCOUNT_OFFSET) as *const AtomicU32;
150        (*refcount_ptr).fetch_add(1, Ordering::Relaxed);
151    }
152}
153
154/// Release a JIT-emitted refcounted heap value.
155///
156/// Atomically decrements the `refcount: AtomicU32` field at offset 4 of
157/// a `UnifiedValue<T>` allocation. When the count reaches zero,
158/// dispatches the kinded free via the `kind: u16` field at offset 0
159/// (the §2.7.6 / Q8 single-discriminator).
160///
161/// The kinded reclaim is delegated to
162/// [`crate::ffi::jit_release::release_unified_value_by_kind`] so the
163/// per-kind `Box::from_raw` arms stay colocated with the
164/// `UnifiedValue<T>` constructors in `jit_kinds.rs` / `value_ffi.rs`.
165///
166/// # Safety
167///
168/// `ptr` must be a non-null `*const UnifiedValue<T>` from a live
169/// JIT-emitted heap allocation, or null. Passing a dangling, mistyped,
170/// or non-`UnifiedValue` pointer is undefined behavior.
171#[unsafe(no_mangle)]
172pub extern "C" fn jit_arc_release(ptr: *const u8) {
173    if ptr.is_null() {
174        return;
175    }
176    JIT_ARC_RELEASE_CALLS.fetch_add(1, Ordering::Relaxed);
177    // SAFETY: see fn docs.
178    unsafe {
179        let refcount_ptr = ptr.add(UNIFIED_VALUE_REFCOUNT_OFFSET) as *const AtomicU32;
180        let prev = (*refcount_ptr).fetch_sub(1, Ordering::Release);
181        if prev == 1 {
182            JIT_ARC_RELEASE_FREES.fetch_add(1, Ordering::Relaxed);
183            // Last reference. Synchronize with all prior fetch_sub
184            // releases (matches the v2 `HeapHeader::release` contract;
185            // necessary so the kinded reclaim sees a consistent view of
186            // the allocation's interior fields).
187            std::sync::atomic::fence(Ordering::Acquire);
188            super::jit_release::release_unified_value_by_kind(ptr);
189        }
190    }
191}