#![allow(unsafe_code)]
#![allow(
dead_code,
reason = "reached through generic dispatch; lib-only build cannot prove reachability"
)]
use core::fmt;
use core::marker::PhantomData;
use core::mem::ManuallyDrop;
use core::ptr::NonNull;
use lean_rs_sys::lean_object;
use lean_rs_sys::refcount::{lean_dec, lean_inc};
use crate::runtime::LeanRuntime;
pub struct Obj<'lean> {
ptr: NonNull<lean_object>,
_life: PhantomData<&'lean LeanRuntime>,
}
pub struct ObjRef<'lean, 'a> {
ptr: NonNull<lean_object>,
_life: PhantomData<(&'lean LeanRuntime, &'a Obj<'lean>)>,
}
impl<'lean> Obj<'lean> {
pub unsafe fn from_owned_raw(_runtime: &'lean LeanRuntime, ptr: *mut lean_object) -> Self {
let ptr = unsafe { NonNull::new_unchecked(ptr) };
Self {
ptr,
_life: PhantomData,
}
}
pub fn into_raw(self) -> *mut lean_object {
ManuallyDrop::new(self).ptr.as_ptr()
}
pub fn as_raw_borrowed(&self) -> *mut lean_object {
self.ptr.as_ptr()
}
pub fn borrow(&self) -> ObjRef<'lean, '_> {
ObjRef {
ptr: self.ptr,
_life: PhantomData,
}
}
#[allow(clippy::unused_self, reason = "`&self` pins the inferred 'lean lifetime parameter")]
pub fn runtime(&self) -> &'lean LeanRuntime {
unsafe { NonNull::<LeanRuntime>::dangling().as_ref() }
}
}
impl ObjRef<'_, '_> {
pub fn as_raw_borrowed(&self) -> *mut lean_object {
self.ptr.as_ptr()
}
}
impl Clone for Obj<'_> {
fn clone(&self) -> Self {
unsafe { lean_inc(self.ptr.as_ptr()) }
Self {
ptr: self.ptr,
_life: PhantomData,
}
}
}
impl Drop for Obj<'_> {
fn drop(&mut self) {
unsafe { lean_dec(self.ptr.as_ptr()) }
}
}
impl fmt::Debug for Obj<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Obj").field("ptr", &self.ptr.as_ptr()).finish()
}
}
impl fmt::Debug for ObjRef<'_, '_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ObjRef").field("ptr", &self.ptr.as_ptr()).finish()
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic)]
use core::ffi::c_char;
use lean_rs_sys::object::{lean_box, lean_is_exclusive, lean_is_shared};
use lean_rs_sys::refcount::lean_dec;
use lean_rs_sys::string::lean_mk_string;
use super::{Obj, ObjRef};
use crate::runtime::LeanRuntime;
fn scalar_obj(runtime: &LeanRuntime) -> Obj<'_> {
unsafe { Obj::from_owned_raw(runtime, lean_box(7)) }
}
fn heap_string(runtime: &LeanRuntime) -> Obj<'_> {
let cstr = c"abc".as_ptr().cast::<c_char>();
unsafe { Obj::from_owned_raw(runtime, lean_mk_string(cstr)) }
}
#[test]
fn scalar_construction_and_drop_is_a_noop() {
let runtime = LeanRuntime::init().expect("runtime init must succeed");
let obj = scalar_obj(runtime);
drop(obj);
}
#[test]
fn clone_increments_heap_refcount() {
let runtime = LeanRuntime::init().expect("runtime init must succeed");
let obj = heap_string(runtime);
assert!(unsafe { lean_is_exclusive(obj.as_raw_borrowed()) });
let copy = obj.clone();
assert!(unsafe { lean_is_shared(obj.as_raw_borrowed()) });
assert!(unsafe { lean_is_shared(copy.as_raw_borrowed()) });
drop(copy);
assert!(unsafe { lean_is_exclusive(obj.as_raw_borrowed()) });
}
#[test]
fn into_raw_does_not_decrement() {
let runtime = LeanRuntime::init().expect("runtime init must succeed");
let obj = heap_string(runtime);
let witness = obj.clone();
assert!(unsafe { lean_is_shared(witness.as_raw_borrowed()) });
let raw = obj.into_raw();
assert!(unsafe { lean_is_shared(witness.as_raw_borrowed()) });
unsafe { lean_dec(raw) };
assert!(unsafe { lean_is_exclusive(witness.as_raw_borrowed()) });
}
#[test]
fn borrow_does_not_adjust_refcount() {
let runtime = LeanRuntime::init().expect("runtime init must succeed");
let obj = heap_string(runtime);
assert!(unsafe { lean_is_exclusive(obj.as_raw_borrowed()) });
let view: ObjRef<'_, '_> = obj.borrow();
let raw = view.as_raw_borrowed();
assert_eq!(raw, obj.as_raw_borrowed());
assert!(unsafe { lean_is_exclusive(obj.as_raw_borrowed()) });
let _ = view;
assert!(unsafe { lean_is_exclusive(obj.as_raw_borrowed()) });
}
#[test]
fn debug_format_renders_pointer_without_dereferencing() {
let runtime = LeanRuntime::init().expect("runtime init must succeed");
let obj = scalar_obj(runtime);
let rendered = format!("{obj:?}");
assert!(rendered.starts_with("Obj"));
assert!(rendered.contains("ptr"));
let view = obj.borrow();
let rendered_ref = format!("{view:?}");
assert!(rendered_ref.starts_with("ObjRef"));
}
#[allow(dead_code, reason = "items are consumed only via trait selection at compile time")]
mod not_send_not_sync {
use super::Obj;
trait AmbiguousIfSend<A> {
fn check() {}
}
impl<T: ?Sized> AmbiguousIfSend<()> for T {}
struct Invalid;
impl<T: ?Sized + Send> AmbiguousIfSend<Invalid> for T {}
trait AmbiguousIfSync<A> {
fn check() {}
}
impl<T: ?Sized> AmbiguousIfSync<()> for T {}
struct InvalidSync;
impl<T: ?Sized + Sync> AmbiguousIfSync<InvalidSync> for T {}
fn _obj_is_not_send() {
<Obj<'static> as AmbiguousIfSend<_>>::check();
}
fn _obj_is_not_sync() {
<Obj<'static> as AmbiguousIfSync<_>>::check();
}
}
fn _lifetime_anchored_to_runtime_borrow(runtime: &LeanRuntime) -> Obj<'_> {
unsafe { Obj::from_owned_raw(runtime, lean_box(0)) }
}
fn stress_iters() -> usize {
std::env::var("LEAN_RS_REFCOUNT_STRESS_ITERS")
.ok()
.and_then(|raw| raw.parse::<usize>().ok())
.unwrap_or(256)
}
#[test]
fn clone_drop_cycle_preserves_exclusive_after_release() {
let runtime = LeanRuntime::init().expect("runtime init must succeed");
let obj = heap_string(runtime);
assert!(unsafe { lean_is_exclusive(obj.as_raw_borrowed()) });
let iters = stress_iters();
for _ in 0..iters {
let copy = obj.clone();
assert!(unsafe { lean_is_shared(copy.as_raw_borrowed()) });
drop(copy);
}
assert!(unsafe { lean_is_exclusive(obj.as_raw_borrowed()) });
}
#[test]
fn many_independent_objs_drop_in_arbitrary_order() {
let runtime = LeanRuntime::init().expect("runtime init must succeed");
let n = stress_iters().clamp(8, 64);
let mut originals: Vec<Obj<'_>> = (0..n).map(|_| heap_string(runtime)).collect();
let clones: Vec<Obj<'_>> = originals.iter().map(Obj::clone).collect();
for clone in &clones {
assert!(unsafe { lean_is_shared(clone.as_raw_borrowed()) });
}
while let Some(orig) = originals.pop() {
drop(orig);
}
for clone in &clones {
assert!(unsafe { lean_is_exclusive(clone.as_raw_borrowed()) });
}
}
#[test]
fn panic_inside_clone_scope_does_not_leak_refcount() {
use std::panic::{self, AssertUnwindSafe};
let runtime = LeanRuntime::init().expect("runtime init must succeed");
let obj = heap_string(runtime);
assert!(unsafe { lean_is_exclusive(obj.as_raw_borrowed()) });
let outcome = panic::catch_unwind(AssertUnwindSafe(|| {
let copy = obj.clone();
assert!(unsafe { lean_is_shared(copy.as_raw_borrowed()) });
panic!("synthetic panic — `copy` must drop on the unwind path");
}));
assert!(outcome.is_err(), "closure was expected to panic");
assert!(unsafe { lean_is_exclusive(obj.as_raw_borrowed()) });
}
}