Skip to main content

lean_rs/abi/
structure.rs

1//! Constructor-object plumbing for product and sum structures.
2//!
3//! The "structure pattern" lives at two primitives, hand-called at each
4//! struct boundary. There is no per-struct trait, no derive, no
5//! procedural macro: callers compose [`alloc_ctor_with_objects`] and
6//! [`take_ctor_objects`] field by field and let the per-field
7//! [`super::traits::IntoLean`] / [`super::traits::TryFromLean`] impls
8//! do the actual type marshalling.
9//!
10//! The module is the only place in `abi` that knows how
11//! [`lean_alloc_ctor`], [`lean_ctor_obj_cptr`], and the constructor's
12//! `tag`/`num_objs` invariants line up; container modules
13//! (`crate::abi::option`, `crate::abi::except`) and downstream
14//! handlers ship through these primitives instead of repeating the
15//! pointer arithmetic. That keeps a single audited copy of the
16//! ctor-allocation rules and centralises the `lean_inc`/`lean_dec`
17//! reasoning.
18//!
19//! ### Lifetime and refcount invariants
20//!
21//! - [`alloc_ctor_with_objects`] consumes the input array of `Obj<'lean>`
22//!   handles. Each handle's owned refcount is transferred—via
23//!   [`Obj::into_raw`]—into the freshly allocated constructor's
24//!   object-slot, so the new parent owns exactly one count per field plus
25//!   its own header count. No `Obj::clone` (which would `lean_inc`) runs
26//!   on the input path.
27//! - [`take_ctor_objects`] reads each object slot once, calls `lean_inc`
28//!   on the field, and wraps the bumped pointer in a fresh `Obj<'lean>`.
29//!   The parent `Obj` is then dropped; its `lean_dec` walks back through
30//!   the original per-field counts, leaving each returned handle with the
31//!   same effective ownership the parent had given that field.
32//! - [`ObjView`] / [`CtorView`] are borrow-only readers for constructor
33//!   tags, scalar-tagged nullary constructors, and scalar-tail fields.
34//!   They never expose raw pointers to callers and never touch the
35//!   refcount.
36
37// SAFETY DOC: every `unsafe { ... }` block in this file carries its own
38// `// SAFETY:` comment naming the invariant; the blanket allow keeps the
39// unsafe surface inside the smallest scope that compiles, per
40// `docs/architecture/01-safety-model.md`.
41#![allow(unsafe_code)]
42
43use core::mem::MaybeUninit;
44
45use lean_rs_sys::ctor::{
46    lean_alloc_ctor, lean_ctor_get_uint8, lean_ctor_get_uint64, lean_ctor_num_objs, lean_ctor_obj_cptr,
47    lean_ctor_scalar_cptr,
48};
49use lean_rs_sys::object::{lean_is_ctor, lean_is_scalar, lean_obj_tag, lean_object_data_byte_size};
50use lean_rs_sys::refcount::lean_inc;
51
52use crate::abi::traits::conversion_error;
53use crate::error::LeanResult;
54use crate::runtime::LeanRuntime;
55use crate::runtime::obj::Obj;
56
57/// Borrowed, allocation-free inspection view over an existing Lean object.
58///
59/// This is the host-facing boundary for Lean's scalar/nullary and
60/// constructor-object representation. Callers can ask whether a value is
61/// scalar-tagged, read a scalar constructor tag, or narrow to
62/// [`CtorView`] before reading constructor header and scalar-tail fields.
63/// The view never transfers ownership and never exposes the underlying
64/// `lean_object*`.
65pub struct ObjView<'lean, 'a> {
66    obj: &'a Obj<'lean>,
67}
68
69/// Borrowed view of a heap-allocated Lean constructor.
70///
71/// Constructed only after [`ObjView::ctor`] has verified that the source
72/// object is a constructor. The cached header facts make repeated reads
73/// cheap and keep scalar-tail bounds checks explicit at each offset.
74#[derive(Copy, Clone)]
75pub struct CtorView<'lean, 'a> {
76    obj: &'a Obj<'lean>,
77    tag: u8,
78    num_object_fields: usize,
79    scalar_tail_size: usize,
80}
81
82/// Build a borrowed view over `obj`.
83#[inline]
84#[must_use]
85pub fn view<'lean, 'a>(obj: &'a Obj<'lean>) -> ObjView<'lean, 'a> {
86    ObjView { obj }
87}
88
89impl<'lean, 'a> ObjView<'lean, 'a> {
90    /// Whether the object is Lean's scalar-tagged pointer form.
91    #[inline]
92    #[must_use]
93    pub fn is_scalar(&self) -> bool {
94        let ptr = self.obj.as_raw_borrowed();
95        // SAFETY: pure pointer-bit inspection.
96        unsafe { lean_is_scalar(ptr) }
97    }
98
99    /// Read the payload of a scalar-tagged object.
100    ///
101    /// This is the representation Lean uses for nullary-only inductive
102    /// values and for some small primitive values (`Nat`, `Bool`, `Unit`
103    /// in boxed positions). The caller supplies `label` only for the
104    /// error message; it is not touched on the success path.
105    ///
106    /// # Errors
107    ///
108    /// Returns a conversion error if the object is heap-allocated.
109    #[inline]
110    pub fn scalar_payload(&self, label: &str) -> LeanResult<usize> {
111        let ptr = self.obj.as_raw_borrowed();
112        // SAFETY: pure pointer-bit inspection.
113        if unsafe { lean_is_scalar(ptr) } {
114            // SAFETY: scalar branch verified above.
115            Ok(unsafe { lean_rs_sys::object::lean_unbox(ptr) })
116        } else {
117            // SAFETY: non-scalar branch; object tag is valid for any live
118            // heap object held by `Obj`.
119            let found_tag = unsafe { lean_obj_tag(ptr) };
120            Err(conversion_error(format!(
121                "expected Lean {label} scalar-tagged object, found heap object with tag {found_tag}"
122            )))
123        }
124    }
125
126    /// Read a sum-constructor tag encoded either as a scalar nullary tag
127    /// or as a heap constructor tag.
128    ///
129    /// This matches Lean's mixed-inductive ABI rule: nullary constructors
130    /// can be scalar-tagged, while constructors with fields are heap
131    /// ctors. The returned tag is the Lean declaration-order constructor
132    /// index.
133    ///
134    /// # Errors
135    ///
136    /// Returns a conversion error if the object is heap-allocated but is
137    /// not a constructor, or if a scalar payload does not fit in `u8`.
138    #[inline]
139    pub fn sum_tag(&self) -> LeanResult<u8> {
140        let ptr = self.obj.as_raw_borrowed();
141        // SAFETY: pure pointer-bit inspection.
142        if unsafe { lean_is_scalar(ptr) } {
143            // SAFETY: scalar branch verified above.
144            let tag = unsafe { lean_rs_sys::object::lean_unbox(ptr) };
145            return u8::try_from(tag)
146                .map_err(|_| conversion_error(format!("Lean scalar constructor tag {tag} does not fit in u8")));
147        }
148        self.ctor().map(|ctor| ctor.tag())
149    }
150
151    /// Narrow this object to a heap-constructor view.
152    ///
153    /// # Errors
154    ///
155    /// Returns a conversion error if the object is scalar-tagged or is a
156    /// non-constructor heap object.
157    #[inline]
158    pub fn ctor(&self) -> LeanResult<CtorView<'lean, 'a>> {
159        CtorView::new(self.obj)
160    }
161
162    /// Narrow this object to a constructor with the expected tag and
163    /// object-slot count.
164    ///
165    /// This is the common generated-result shape check. It validates the
166    /// constructor tag and object-field arity before any caller reads
167    /// scalar-tail values or consumes fields through [`take_ctor_objects`].
168    ///
169    /// # Errors
170    ///
171    /// Returns a conversion error if the object is not a constructor, or
172    /// if the constructor tag or object-field count differs.
173    #[inline]
174    pub fn ctor_shape(
175        &self,
176        expected_tag: u8,
177        expected_num_object_fields: usize,
178        label: &str,
179    ) -> LeanResult<CtorView<'lean, 'a>> {
180        self.ctor()?
181            .require_shape(expected_tag, expected_num_object_fields, label)
182    }
183}
184
185impl<'lean, 'a> CtorView<'lean, 'a> {
186    #[inline]
187    fn new(obj: &'a Obj<'lean>) -> LeanResult<Self> {
188        let ptr = obj.as_raw_borrowed();
189        // SAFETY: pure pointer-bit inspection.
190        if unsafe { lean_is_scalar(ptr) } {
191            return Err(conversion_error(
192                "expected Lean constructor, found scalar-tagged object",
193            ));
194        }
195        // SAFETY: non-scalar; ctor predicate inspects the header tag only.
196        if !unsafe { lean_is_ctor(ptr) } {
197            // SAFETY: same branch.
198            let found_tag = unsafe { lean_obj_tag(ptr) };
199            return Err(conversion_error(format!(
200                "expected Lean constructor, found object with tag {found_tag}"
201            )));
202        }
203        // SAFETY: ctor object—its tag fits a `u8` per Lean's
204        // `LEAN_MAX_CTOR_TAG` ceiling, and `m_other` holds the object
205        // field count for ctors.
206        let tag = unsafe { lean_obj_tag(ptr) };
207        let num_object_fields = unsafe { lean_ctor_num_objs(ptr) } as usize;
208        #[allow(
209            clippy::cast_possible_truncation,
210            reason = "ctor tag is bounded by LEAN_MAX_CTOR_TAG"
211        )]
212        let tag = tag as u8;
213        let scalar_tail_size = ctor_scalar_tail_size(ptr);
214        Ok(Self {
215            obj,
216            tag,
217            num_object_fields,
218            scalar_tail_size,
219        })
220    }
221
222    /// Constructor tag in Lean declaration order.
223    #[inline]
224    #[must_use]
225    pub fn tag(&self) -> u8 {
226        self.tag
227    }
228
229    #[inline]
230    fn require_tag(self, expected_tag: u8, label: &str) -> LeanResult<Self> {
231        if self.tag == expected_tag {
232            Ok(self)
233        } else {
234            Err(conversion_error(format!(
235                "expected Lean {label} ctor (tag {expected_tag}), found tag {}",
236                self.tag
237            )))
238        }
239    }
240
241    #[inline]
242    fn require_shape(self, expected_tag: u8, expected_num_object_fields: usize, label: &str) -> LeanResult<Self> {
243        let this = self.require_tag(expected_tag, label)?;
244        if this.num_object_fields == expected_num_object_fields {
245            Ok(this)
246        } else {
247            Err(conversion_error(format!(
248                "expected Lean {label} ctor with {expected_num_object_fields} object field(s), found {}",
249                this.num_object_fields
250            )))
251        }
252    }
253
254    /// Read a `UInt8` field from the constructor scalar tail at byte
255    /// `offset`.
256    ///
257    /// # Errors
258    ///
259    /// Returns a conversion error if the requested byte range is outside
260    /// the scalar tail.
261    #[inline]
262    pub fn uint8(&self, offset: u32, label: &str) -> LeanResult<u8> {
263        self.require_scalar_tail(offset, 1, label)?;
264        let ptr = self.obj.as_raw_borrowed();
265        // SAFETY: constructor kind was validated by `CtorView::new`, and
266        // the explicit bounds check above proves `offset..offset+1` lies
267        // inside the scalar tail.
268        Ok(unsafe { lean_ctor_get_uint8(ptr, offset) })
269    }
270
271    /// Read a `UInt64` field from the constructor scalar tail at byte
272    /// `offset`.
273    ///
274    /// # Errors
275    ///
276    /// Returns a conversion error if the requested byte range is outside
277    /// the scalar tail.
278    #[inline]
279    pub fn uint64(&self, offset: u32, label: &str) -> LeanResult<u64> {
280        self.require_scalar_tail(offset, core::mem::size_of::<u64>(), label)?;
281        let ptr = self.obj.as_raw_borrowed();
282        // SAFETY: constructor kind was validated by `CtorView::new`, and
283        // the explicit bounds check above proves `offset..offset+8` lies
284        // inside the scalar tail. The sys helper performs an unaligned
285        // read, matching Lean's C accessor.
286        Ok(unsafe { lean_ctor_get_uint64(ptr, offset) })
287    }
288
289    /// Decode a `Bool`-encoded scalar-tail byte.
290    ///
291    /// # Errors
292    ///
293    /// Returns a conversion error if the byte is outside the scalar tail
294    /// or is not `0` / `1`.
295    #[inline]
296    pub fn bool(&self, offset: u32, label: &str) -> LeanResult<bool> {
297        match self.uint8(offset, label)? {
298            0 => Ok(false),
299            1 => Ok(true),
300            other => Err(conversion_error(format!(
301                "Lean {label} byte {other} is not in {{0, 1}}"
302            ))),
303        }
304    }
305
306    #[inline]
307    fn require_scalar_tail(&self, offset: u32, width: usize, label: &str) -> LeanResult<()> {
308        let start = offset as usize;
309        let Some(end) = start.checked_add(width) else {
310            return Err(conversion_error(format!(
311                "Lean {label} scalar-tail read at offset {offset} overflows usize"
312            )));
313        };
314        if end <= self.scalar_tail_size {
315            Ok(())
316        } else {
317            Err(conversion_error(format!(
318                "Lean {label} scalar-tail read at offset {offset} for {width} byte(s) exceeds scalar tail size {}",
319                self.scalar_tail_size
320            )))
321        }
322    }
323}
324
325/// Allocate a freshly-initialised constructor with `N` object-pointer
326/// fields and no scalar payload.
327///
328/// `tag` is the inductive constructor index (Lean's declaration order:
329/// `Option.none` = 0, `Option.some` = 1, `Except.error` = 0, `Except.ok`
330/// = 1, …). Each entry of `objects` is moved into its slot via
331/// [`Obj::into_raw`], so the returned [`Obj`] owns the only live refcount
332/// per field plus its own header count. The const-generic `N` matches the
333/// number of object-pointer slots the Lean inductive declares, which
334/// keeps the call site self-documenting and lets the compiler refuse
335/// arity mismatches.
336///
337/// # Panics
338///
339/// Panics only via `lean_alloc_ctor`'s `strict_*` arithmetic overflow
340/// guard—unreachable for the constructor shapes Lean emits
341/// (`LEAN_MAX_CTOR_FIELDS` = 256).
342pub fn alloc_ctor_with_objects<'lean, const N: usize>(
343    runtime: &'lean LeanRuntime,
344    tag: u8,
345    objects: [Obj<'lean>; N],
346) -> Obj<'lean> {
347    // SAFETY: `lean_alloc_ctor` returns a fresh ctor with refcount 1 and
348    // `N` uninitialised object slots; we write each `Obj::into_raw` into
349    // its slot before the object escapes, satisfying the
350    // "fully initialise every declared field" obligation.
351    unsafe {
352        // The `num_objs` parameter is `u8`; assert at compile time that
353        // `N` fits, matching `lean.h`'s `LEAN_MAX_CTOR_FIELDS` ceiling
354        // (the const block evaluates at type-checking time).
355        const { assert!(N <= u8::MAX as usize, "ctor arity exceeds Lean's u8 num_objs field") };
356        let raw = lean_alloc_ctor(tag, N as u8, 0);
357        let slots = lean_ctor_obj_cptr(raw);
358        for (i, field) in objects.into_iter().enumerate() {
359            *slots.add(i) = field.into_raw();
360        }
361        Obj::from_owned_raw(runtime, raw)
362    }
363}
364
365/// Validate that `obj` is a constructor with `expected_tag` and exactly
366/// `N` object-pointer fields, then return the `N` owned field handles.
367///
368/// Each returned [`Obj`] carries one refcount: [`lean_inc`] is called on
369/// the slot pointer before wrapping it. The parent `obj` is consumed and
370/// its [`Drop`] runs the matching `lean_dec` (which decrements each field
371/// once more—balancing the `lean_inc`s and leaving the returned handles
372/// with the same effective ownership the parent originally held).
373///
374/// `label` is embedded in the diagnostic on failure so callers see
375/// `"expected Lean Option::some ctor (tag 1, num_objs 1), …"` rather
376/// than an anonymous "wrong ctor".
377///
378/// # Errors
379///
380/// Returns [`HostStage::Conversion`](crate::error::HostStage::Conversion)
381/// if `obj` is scalar-tagged, has a non-constructor heap tag, has a
382/// different tag from `expected_tag`, or carries a different
383/// object-slot count from `N`.
384pub fn take_ctor_objects<'lean, const N: usize>(
385    obj: Obj<'lean>,
386    expected_tag: u8,
387    label: &str,
388) -> LeanResult<[Obj<'lean>; N]> {
389    require_ctor_shape(&obj, expected_tag, N, label)?;
390    let runtime = obj.runtime();
391    let ptr = obj.as_raw_borrowed();
392    // SAFETY: shape validated above; `lean_ctor_obj_cptr` returns a
393    // pointer valid for `N` slots, each holding a live owned
394    // `*mut lean_object` (well-formed Lean ctor invariant).
395    let slots = unsafe { lean_ctor_obj_cptr(ptr) };
396    let mut out: [MaybeUninit<Obj<'lean>>; N] = [const { MaybeUninit::uninit() }; N];
397    for (i, slot) in out.iter_mut().enumerate() {
398        // SAFETY: index in `0..N` is in-bounds per the shape check; the
399        // slot read is a borrowed view, then `lean_inc` bumps the refcount
400        // so the wrapped `Obj` owns its own count independent of the
401        // parent.
402        unsafe {
403            let field_ptr = *slots.add(i);
404            lean_inc(field_ptr);
405            slot.write(Obj::from_owned_raw(runtime, field_ptr));
406        }
407    }
408    // The parent `obj` falls out of scope here; its `Drop` releases the
409    // original constructor (and the per-field counts the parent held).
410    drop(obj);
411    // SAFETY: every element of `out` was initialised in the loop above
412    // (`0..N` covers the whole array exactly once); transmute is sound
413    // because `[MaybeUninit<T>; N]` and `[T; N]` share layout.
414    Ok(out.map(|cell| unsafe { cell.assume_init() }))
415}
416
417/// Read the tag byte of a constructor object.
418///
419/// Used by sum-type decoders (`Option` and `except::Except` (sibling-module sum-type carriers))
420/// that need to pick a variant before they know the arity. Borrow-only:
421/// leaves the refcount untouched.
422///
423/// # Errors
424///
425/// Returns [`HostStage::Conversion`](crate::error::HostStage::Conversion)
426/// if `obj` is not a heap-allocated constructor.
427pub fn ctor_tag(obj: &Obj<'_>) -> LeanResult<u8> {
428    view(obj).ctor().map(|ctor| ctor.tag())
429}
430
431/// Shared validator for [`take_ctor_objects`]: ctor kind, matching tag,
432/// matching `num_objs`.
433fn require_ctor_shape(obj: &Obj<'_>, expected_tag: u8, expected_num_objs: usize, label: &str) -> LeanResult<()> {
434    let _ = view(obj).ctor_shape(expected_tag, expected_num_objs, label)?;
435    Ok(())
436}
437
438#[inline]
439fn ctor_scalar_tail_size(ptr: *mut lean_rs_sys::lean_object) -> usize {
440    // SAFETY: callers have validated `ptr` is a constructor. The scalar
441    // tail starts immediately after the object-pointer slots. Lean's
442    // object-data-byte-size helper reports the initialized value
443    // representation span for the object shape produced by
444    // `lean_alloc_ctor`, so the difference is the readable scalar-tail
445    // length.
446    unsafe {
447        let scalar_start = lean_ctor_scalar_cptr(ptr) as usize;
448        let object_start = ptr as usize;
449        let scalar_offset = scalar_start.saturating_sub(object_start);
450        lean_object_data_byte_size(ptr).saturating_sub(scalar_offset)
451    }
452}
453
454#[cfg(test)]
455mod tests {
456    #![allow(clippy::expect_used)]
457
458    use core::ffi::c_char;
459
460    use lean_rs_sys::ctor::{lean_alloc_ctor, lean_ctor_set_uint8, lean_ctor_set_uint64};
461    use lean_rs_sys::object::{lean_box, lean_is_exclusive, lean_is_shared};
462    use lean_rs_sys::string::lean_mk_string;
463
464    use super::{alloc_ctor_with_objects, take_ctor_objects, view};
465    use crate::runtime::LeanRuntime;
466    use crate::runtime::obj::Obj;
467
468    fn runtime() -> &'static LeanRuntime {
469        LeanRuntime::init().expect("runtime init must succeed")
470    }
471
472    fn scalar_obj(runtime: &LeanRuntime, payload: usize) -> Obj<'_> {
473        // SAFETY: `lean_box` is pointer-bit construction; scalar-tagged
474        // objects are valid `Obj` handles and refcount operations are no-ops.
475        unsafe { Obj::from_owned_raw(runtime, lean_box(payload)) }
476    }
477
478    fn heap_string(runtime: &LeanRuntime) -> Obj<'_> {
479        let cstr = c"field".as_ptr().cast::<c_char>();
480        // SAFETY: `cstr` is a static NUL-terminated UTF-8 string, and
481        // `lean_mk_string` returns an owned Lean object.
482        unsafe { Obj::from_owned_raw(runtime, lean_mk_string(cstr)) }
483    }
484
485    fn ctor_with_scalar_tail(runtime: &LeanRuntime) -> Obj<'_> {
486        // SAFETY: allocate a ctor with no object fields and 16 scalar
487        // bytes, then initialize the bytes read by the test before the
488        // object escapes.
489        unsafe {
490            let raw = lean_alloc_ctor(2, 0, 16);
491            lean_ctor_set_uint8(raw, 0, 1);
492            lean_ctor_set_uint64(raw, 8, 0x0102_0304_0506_0708);
493            Obj::from_owned_raw(runtime, raw)
494        }
495    }
496
497    #[test]
498    fn view_discriminates_scalar_and_constructor() {
499        let runtime = runtime();
500        let scalar = scalar_obj(runtime, 3);
501        assert!(view(&scalar).is_scalar());
502        assert_eq!(view(&scalar).scalar_payload("TestScalar").expect("scalar payload"), 3);
503        assert_eq!(view(&scalar).sum_tag().expect("scalar sum tag"), 3);
504
505        let ctor = alloc_ctor_with_objects(runtime, 1, []);
506        let ctor_view = view(&ctor).ctor().expect("ctor view");
507        assert!(!view(&ctor).is_scalar());
508        assert_eq!(ctor_view.tag(), 1);
509    }
510
511    #[test]
512    fn constructor_shape_checks_tag_and_object_field_count() {
513        let runtime = runtime();
514        let ctor = alloc_ctor_with_objects(runtime, 4, [scalar_obj(runtime, 9)]);
515        let ctor_view = view(&ctor).ctor_shape(4, 1, "OneField").expect("expected ctor shape");
516        assert_eq!(ctor_view.tag(), 4);
517
518        assert!(view(&ctor).ctor_shape(5, 1, "OneField").is_err());
519        assert!(view(&ctor).ctor_shape(4, 2, "OneField").is_err());
520    }
521
522    #[test]
523    fn scalar_tail_reads_are_bounds_checked() {
524        let runtime = runtime();
525        let ctor = ctor_with_scalar_tail(runtime);
526        let ctor_view = view(&ctor).ctor_shape(2, 0, "ScalarTail").expect("expected ctor shape");
527
528        assert!(ctor_view.bool(0, "ScalarTail.flag").expect("bool tail"));
529        assert_eq!(
530            ctor_view.uint64(8, "ScalarTail.count").expect("u64 tail"),
531            0x0102_0304_0506_0708,
532        );
533        assert!(ctor_view.uint64(9, "ScalarTail.count").is_err());
534        assert!(ctor_view.uint8(16, "ScalarTail.flag").is_err());
535    }
536
537    #[test]
538    fn malformed_object_shape_errors_without_panicking() {
539        let runtime = runtime();
540        let scalar = scalar_obj(runtime, 0);
541        assert!(view(&scalar).ctor().is_err());
542
543        let wide_scalar = scalar_obj(runtime, usize::from(u8::MAX) + 1);
544        assert!(view(&wide_scalar).sum_tag().is_err());
545
546        let ctor = ctor_with_scalar_tail(runtime);
547        assert!(view(&ctor).scalar_payload("ExpectedScalar").is_err());
548    }
549
550    #[test]
551    fn take_ctor_objects_preserves_field_ownership() {
552        let runtime = runtime();
553        let child = heap_string(runtime);
554        let witness = child.clone();
555
556        let parent = alloc_ctor_with_objects(runtime, 0, [child]);
557        let [taken] = take_ctor_objects::<1>(parent, 0, "Parent").expect("take field");
558
559        // SAFETY: header-only refcount observations of live owned objects.
560        assert!(unsafe { lean_is_shared(taken.as_raw_borrowed()) });
561        assert!(unsafe { lean_is_shared(witness.as_raw_borrowed()) });
562
563        drop(taken);
564        // SAFETY: after dropping the extracted field, only `witness`
565        // remains. If `take_ctor_objects` failed to balance the parent
566        // and child refcounts, this would stay shared or ASan would catch
567        // the double-release path.
568        assert!(unsafe { lean_is_exclusive(witness.as_raw_borrowed()) });
569        drop(witness);
570    }
571}