Skip to main content

lean_rs_sys/
refcount.rs

1//! Refcount fast paths—Rust mirrors of `lean.h:536–563`.
2//!
3//! The header encodes the runtime mode in the RC sign:
4//! - `m_rc > 0` —single-threaded: bump or decrement in place.
5//! - `m_rc < 0` —multi-threaded: RC is negated; `fetch_sub` is the relaxed
6//!   atomic decrement, mirroring `atomic_fetch_sub_explicit(.., relaxed)` in
7//!   `lean.h`.
8//! - `m_rc == 0`—persistent (compact regions); never refcounted.
9//!
10//! Each mirror reaches `m_rc` through [`AtomicI32::from_ptr`] so the actual
11//! load / store / `fetch_sub` call site sees a safe `&AtomicI32`.
12
13// These mirrors of `lean.h`'s `static inline` helpers must inline through the
14// FFI boundary for the fast paths to be free; that is the design.
15#![allow(clippy::inline_always)]
16
17use core::sync::atomic::{AtomicI32, Ordering};
18
19use crate::object::lean_is_scalar;
20use crate::repr::LeanObjectRepr;
21use crate::types::lean_object;
22
23unsafe extern "C" {
24    /// Cold path for refcount-zero decrement (`lean.h:552`). Invoked by
25    /// [`lean_dec_ref`] when the live count would cross to zero and the
26    /// object actually needs freeing.
27    pub fn lean_dec_ref_cold(o: *mut lean_object);
28
29    /// Mark a heap object—and everything reachable from it—as
30    /// multi-threaded (`lean.h:612`).
31    pub fn lean_mark_mt(o: *mut lean_object);
32
33    /// Mark a heap object as persistent so its refcount is no longer
34    /// updated (`lean.h:613`).
35    pub fn lean_mark_persistent(o: *mut lean_object);
36}
37
38#[inline(always)]
39unsafe fn rc_atom<'a>(o: *mut lean_object) -> &'a AtomicI32 {
40    // SAFETY: caller guarantees `o` is a valid non-scalar Lean heap object.
41    // `LeanObjectRepr`'s layout is pinned by `LEAN_HEADER_DIGEST`; the
42    // `m_rc` field is at offset 0. `AtomicI32::from_ptr` requires the
43    // pointer to be aligned (Lean allocates `lean_object`s with at least
44    // 4-byte alignment—pinned by the same digest) and valid for shared
45    // access for the duration of `'a`.
46    unsafe {
47        let repr = o.cast::<LeanObjectRepr>();
48        AtomicI32::from_ptr(&raw mut (*repr).m_rc)
49    }
50}
51
52/// Bump `o`'s reference count by `n` (`lean.h:536–546`, `lean_inc_ref_n`).
53///
54/// # Safety
55///
56/// `o` must be a valid non-scalar Lean object pointer per the `lean_obj_arg`
57/// calling convention; the caller must ensure the increment cannot overflow
58/// (Lean's runtime preserves this invariant by construction). The layout of
59/// `lean_object` matches the crate-private `LeanObjectRepr` per the
60/// build-time digest check.
61///
62/// # Panics
63///
64/// Panics if adding `n` to the single-threaded refcount would overflow
65/// `i32`. This indicates a runtime invariant breach (an object referenced
66/// >2 billion times) rather than a recoverable condition.
67#[inline(always)]
68pub unsafe fn lean_inc_ref_n(o: *mut lean_object, n: usize) {
69    // SAFETY: precondition above; `n` fits in i32 in any realistic program.
70    let rc = unsafe { rc_atom(o) };
71    let cur = rc.load(Ordering::Relaxed);
72    let delta = n as i32;
73    if cur > 0 {
74        // `strict_add` panics on overflow in both debug and release; a
75        // refcount overflow indicates a runtime invariant breach (a single
76        // object held by >2 billion references), not a recoverable
77        // condition.
78        rc.store(cur.strict_add(delta), Ordering::Relaxed);
79    } else if cur != 0 {
80        // Multi-threaded: m_rc is negated, so increment-by-n is fetch_sub(n).
81        rc.fetch_sub(delta, Ordering::Relaxed);
82    }
83}
84
85/// Bump `o`'s refcount by one (`lean.h:548–550`).
86///
87/// # Safety
88///
89/// Same as [`lean_inc_ref_n`].
90#[inline(always)]
91pub unsafe fn lean_inc_ref(o: *mut lean_object) {
92    // SAFETY: forwards to lean_inc_ref_n with the same precondition.
93    unsafe { lean_inc_ref_n(o, 1) }
94}
95
96/// Bump `o`'s refcount by one, or no-op for scalar-tagged pointers
97/// (`lean.h:561`).
98///
99/// # Safety
100///
101/// `o` must be either a scalar-tagged pointer (low bit set) or a valid Lean
102/// heap object. The scalar branch is unconditionally safe; the heap branch
103/// inherits [`lean_inc_ref`]'s precondition.
104#[inline(always)]
105pub unsafe fn lean_inc(o: *mut lean_object) {
106    // SAFETY: scalar check inspects pointer bits only; the heap branch
107    // inherits `lean_inc_ref`'s precondition (non-scalar object).
108    unsafe {
109        if !lean_is_scalar(o) {
110            lean_inc_ref(o);
111        }
112    }
113}
114
115/// Bump `o`'s refcount by `n`, or no-op for scalar-tagged pointers
116/// (`lean.h:562`).
117///
118/// # Safety
119///
120/// Same as [`lean_inc`].
121#[inline(always)]
122pub unsafe fn lean_inc_n(o: *mut lean_object, n: usize) {
123    // SAFETY: scalar check inspects pointer bits only; the heap branch
124    // inherits `lean_inc_ref_n`'s precondition (non-scalar object).
125    unsafe {
126        if !lean_is_scalar(o) {
127            lean_inc_ref_n(o, n);
128        }
129    }
130}
131
132/// Decrement `o`'s refcount, taking the cold path to free if it reaches zero
133/// (`lean.h:554–560`).
134///
135/// # Safety
136///
137/// `o` must be a valid non-scalar Lean heap object. The runtime invariant
138/// that `m_rc == 0` means "persistent, no refcounting" is honoured: this
139/// function is a no-op in that case.
140///
141/// # Panics
142///
143/// The single-threaded fast path only runs when `m_rc > 1`, so subtracting
144/// 1 cannot underflow; the panic guard exists to catch a future invariant
145/// breach (e.g. unsynchronized concurrent mutation) rather than a
146/// recoverable condition.
147#[inline(always)]
148pub unsafe fn lean_dec_ref(o: *mut lean_object) {
149    // SAFETY: precondition above; cold path is `LEAN_EXPORT`'d.
150    let rc = unsafe { rc_atom(o) };
151    let cur = rc.load(Ordering::Relaxed);
152    if cur > 1 {
153        // `cur > 1` so subtracting 1 cannot wrap; `strict_sub` surfaces any
154        // future invariant breach as a panic instead of silently wrapping.
155        rc.store(cur.strict_sub(1), Ordering::Relaxed);
156    } else if cur != 0 {
157        // Either ST with cur==1, or MT with negated count whose abs value is
158        // the live count. The C source calls `lean_dec_ref_cold` in either
159        // case; the cold path handles MT subtraction internally.
160        // SAFETY: cold path is the only safe way to free; it takes ownership.
161        unsafe { lean_dec_ref_cold(o) }
162    }
163}
164
165/// Decrement `o`'s refcount, or no-op for scalar-tagged pointers
166/// (`lean.h:563`).
167///
168/// # Safety
169///
170/// Same as [`lean_inc`].
171#[inline(always)]
172pub unsafe fn lean_dec(o: *mut lean_object) {
173    // SAFETY: scalar check inspects pointer bits only; heap branch inherits
174    // `lean_dec_ref`'s precondition (non-scalar Lean heap object).
175    unsafe {
176        if !lean_is_scalar(o) {
177            lean_dec_ref(o);
178        }
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    //! Pure-Rust coverage of the refcount fast paths over a synthetic,
185    //! Rust-owned object header. These never cross the refcount to zero, so
186    //! the `lean_dec_ref_cold` extern is never reached and the tests run
187    //! unchanged under `cargo miri test`—where they validate the
188    //! `AtomicI32::from_ptr` provenance and alignment the FFI relies on.
189
190    use super::{lean_dec, lean_dec_ref, lean_inc, lean_inc_n, lean_inc_ref, lean_inc_ref_n};
191    use crate::object::lean_box;
192    use crate::repr::test_support::MockObject;
193
194    #[test]
195    fn single_threaded_inc_dec_round_trips() {
196        let obj = MockObject::new(1, 0, 0);
197        let o = obj.ptr();
198        // SAFETY: `o` is a valid non-scalar header; rc stays > 0 throughout,
199        // so the decrement never reaches the cold path.
200        unsafe {
201            lean_inc_ref(o);
202            assert_eq!(obj.rc(), 2);
203            lean_dec_ref(o);
204            assert_eq!(obj.rc(), 1);
205        }
206    }
207
208    #[test]
209    fn inc_ref_n_adds_in_place() {
210        let obj = MockObject::new(1, 0, 0);
211        let o = obj.ptr();
212        // SAFETY: as above; only the single-threaded store path runs.
213        unsafe {
214            lean_inc_ref_n(o, 9);
215        }
216        assert_eq!(obj.rc(), 10);
217    }
218
219    #[test]
220    fn multi_threaded_inc_decrements_negated_count() {
221        // Multi-threaded objects store the live count negated; an increment is
222        // a `fetch_sub`, so the field moves further negative.
223        let obj = MockObject::new(-1, 0, 0);
224        let o = obj.ptr();
225        // SAFETY: valid non-scalar header; the MT branch is a relaxed
226        // `fetch_sub` with no cold-path dispatch.
227        unsafe {
228            lean_inc_ref(o);
229        }
230        assert_eq!(obj.rc(), -2);
231    }
232
233    #[test]
234    fn scalar_pointers_are_left_untouched() {
235        // A scalar-tagged pointer never aliases an allocation; inc/dec must be
236        // pure pointer-bit no-ops that touch no memory.
237        // SAFETY: `lean_box` is pointer arithmetic; `lean_inc`/`lean_dec`
238        // inspect the scalar tag and return without dereferencing.
239        unsafe {
240            let scalar = lean_box(42);
241            lean_inc(scalar);
242            lean_inc_n(scalar, 7);
243            lean_dec(scalar);
244            assert_eq!(scalar, lean_box(42));
245        }
246    }
247}