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}