Skip to main content

lean_rs_sys/
ctor.rs

1//! Constructor objects and polymorphic boxing—Rust mirrors of
2//! `lean.h:642–760` and the `lean_box_uint*` / `lean_box_float` family at
3//! `lean.h:2811–2873`.
4//!
5//! Lean's polymorphic boxing for fixed-width unboxed values (`UInt32` on
6//! 32-bit, `UInt64`, `USize`, `Float`, `Float32`) wraps the value in a
7//! single-field constructor (`tag=0`, `num_objs=0`, scalar payload
8//! sized for the value). The boxed form is the representation an
9//! `Array UIntN` / `Option UIntN` / `Except E UIntN` field carries; the
10//! direct unboxed form is reserved for argument and return positions in
11//! exported Lean functions.
12//!
13//! Each helper here threads through the `lean_alloc_object` extern declared
14//! in [`crate::object`] and writes header bytes via the crate-private
15//! `LeanCtorObjectRepr` layout in `crate::repr`. The pinned
16//! `EXPECTED_HEADER_DIGEST` guarantees the field offsets match the active
17//! `lean.h`.
18
19#![allow(clippy::inline_always)]
20
21use core::mem::size_of;
22
23use crate::consts::{LEAN_MAX_SMALL_OBJECT_SIZE, LEAN_OBJECT_SIZE_DELTA};
24use crate::object::lean_alloc_object;
25use crate::repr::{LeanCtorObjectRepr, LeanObjectRepr};
26use crate::types::{b_lean_obj_arg, lean_obj_res, lean_object};
27
28/// Write the single-threaded header (`m_rc=1`, `m_tag`, `m_other`) on a
29/// freshly allocated object—Rust mirror of `lean_set_st_header`
30/// (`lean.h:615–623`).
31///
32/// Constructor allocation uses Lean's small-object sizing convention:
33/// `m_cs_sz` stores the aligned object byte size for live small objects.
34/// The exported `lean_alloc_object` symbol does not initialise that byte
35/// for callers that bypass Lean's inline `lean_alloc_ctor_memory`, so the
36/// Rust mirror writes it here after allocation. Without this write,
37/// `lean_object_byte_size` cannot recover the byte span of Rust-allocated
38/// constructors, which in turn prevents safe scalar-tail bounds checks.
39///
40/// # Safety
41///
42/// `o` must point to a freshly allocated, otherwise-uninitialized Lean
43/// heap object whose layout matches [`LeanObjectRepr`].
44#[inline(always)]
45unsafe fn set_st_header(o: *mut lean_object, tag: u8, other: u8, aligned_size: u16) {
46    // SAFETY: precondition above; layout pinned by `EXPECTED_HEADER_DIGEST`.
47    unsafe {
48        let repr = o.cast::<LeanObjectRepr>();
49        (*repr).m_rc = 1;
50        (*repr).m_cs_sz = aligned_size;
51        (*repr).m_tag = tag;
52        (*repr).m_other = other;
53    }
54}
55
56#[inline(always)]
57fn align_small_object_size(sz: usize) -> usize {
58    sz.strict_add(LEAN_OBJECT_SIZE_DELTA - 1) & !(LEAN_OBJECT_SIZE_DELTA - 1)
59}
60
61/// Number of object-pointer fields stored in a constructor (`lean.h:644`).
62///
63/// # Safety
64///
65/// `o` must be a borrowed Lean constructor object.
66#[inline(always)]
67pub unsafe fn lean_ctor_num_objs(o: b_lean_obj_arg) -> u8 {
68    // SAFETY: precondition above; `m_other` is the num-objs field for ctor
69    // objects (`lean.h:129`).
70    unsafe { crate::object::lean_ptr_other(o) }
71}
72
73/// Pointer to the constructor's object-field storage (`lean.h:649–652`).
74///
75/// # Safety
76///
77/// `o` must be a borrowed Lean constructor object. The returned pointer is
78/// valid for `lean_ctor_num_objs(o)` `*mut lean_object` slots.
79#[inline(always)]
80pub unsafe fn lean_ctor_obj_cptr(o: *mut lean_object) -> *mut *mut lean_object {
81    // SAFETY: precondition above; flexible-array member at fixed offset.
82    unsafe { (&raw mut (*o.cast::<LeanCtorObjectRepr>()).objs).cast::<*mut lean_object>() }
83}
84
85/// Pointer to the constructor's scalar payload storage
86/// (`lean.h:654–657`). Sits immediately past the object-pointer slots.
87///
88/// # Safety
89///
90/// `o` must be a borrowed Lean constructor object whose scalar payload area
91/// is at least `offset + size_of::<T>()` bytes wide for any `offset` the
92/// caller subsequently passes to `lean_ctor_get_*` / `lean_ctor_set_*`.
93#[inline(always)]
94pub unsafe fn lean_ctor_scalar_cptr(o: *mut lean_object) -> *mut u8 {
95    // SAFETY: precondition above; the scalar area starts one element past
96    // the last `*mut lean_object` slot.
97    unsafe {
98        let num_objs = lean_ctor_num_objs(o) as usize;
99        lean_ctor_obj_cptr(o).add(num_objs).cast::<u8>()
100    }
101}
102
103/// Allocate a freshly initialized constructor object—Rust mirror of
104/// `lean_alloc_ctor` (`lean.h:659–664`).
105///
106/// `tag` selects the constructor (`0..=LEAN_MAX_CTOR_TAG`), `num_objs`
107/// names how many object-pointer fields it carries, and `scalar_sz` is the
108/// byte width of the appended scalar payload. The returned object has
109/// `m_rc=1`; the caller subsequently initialises every object field via
110/// [`lean_ctor_obj_cptr`] writes and every scalar field via
111/// `lean_ctor_set_*`.
112///
113/// # Safety
114///
115/// All three sizing parameters must fit `lean.h`'s
116/// `LEAN_MAX_CTOR_TAG` / `LEAN_MAX_CTOR_FIELDS` / `LEAN_MAX_CTOR_SCALARS_SIZE`
117/// ceilings. The caller must fully initialise every declared field before
118/// passing the object to other Lean routines (notably the object-pointer
119/// fields, which Lean's RC machinery will otherwise read as garbage).
120///
121/// # Panics
122///
123/// Panics if the computed small-object size exceeds Lean's
124/// `LEAN_MAX_SMALL_OBJECT_SIZE`; this indicates a caller violated the
125/// sizing preconditions above.
126#[inline(always)]
127pub unsafe fn lean_alloc_ctor(tag: u8, num_objs: u8, scalar_sz: usize) -> lean_obj_res {
128    let sz = size_of::<LeanObjectRepr>()
129        .strict_add(size_of::<*mut lean_object>().strict_mul(num_objs as usize))
130        .strict_add(scalar_sz);
131    let aligned_sz = align_small_object_size(sz);
132    assert!(aligned_sz <= LEAN_MAX_SMALL_OBJECT_SIZE);
133    #[allow(
134        clippy::cast_possible_truncation,
135        reason = "LEAN_MAX_SMALL_OBJECT_SIZE is below u16::MAX"
136    )]
137    let aligned_sz = aligned_sz as u16;
138    // SAFETY: `lean_alloc_object` returns a non-null pointer to `sz` bytes of
139    // uninitialised Lean-managed memory; we immediately install the
140    // single-threaded header so any subsequent access through Lean's
141    // predicates sees a well-formed object.
142    unsafe {
143        let o = lean_alloc_object(sz);
144        set_st_header(o, tag, num_objs, aligned_sz);
145        o
146    }
147}
148
149/// Box a `u32` as a single-field constructor (`lean.h:2813–2823`).
150///
151/// On 64-bit hosts Lean's `UInt32` is already representable as a
152/// scalar-tagged pointer via [`crate::object::lean_box`]; this helper is
153/// the polymorphic-boxed form needed when a `UInt32` value lands in a
154/// constructor field of an `Array UInt32` / `Option UInt32` / etc.
155///
156/// # Safety
157///
158/// Pure pointer arithmetic plus one `lean_alloc_object` call; no caller
159/// preconditions.
160#[inline(always)]
161pub unsafe fn lean_box_uint32(v: u32) -> lean_obj_res {
162    // SAFETY: ctor allocation is unconditional; we initialise the single
163    // scalar payload before returning.
164    unsafe {
165        let o = lean_alloc_ctor(0, 0, size_of::<u32>());
166        lean_ctor_set_uint32(o, 0, v);
167        o
168    }
169}
170
171/// Recover the `u32` payload from a constructor produced by
172/// [`lean_box_uint32`] (`lean.h:2825–2833`).
173///
174/// # Safety
175///
176/// `o` must be a borrowed constructor object produced by
177/// [`lean_box_uint32`] (or by Lean's compiler in a polymorphic position
178/// holding a `UInt32`). On 64-bit hosts, scalar-tagged `o` is read
179/// directly via [`crate::object::lean_unbox`] instead.
180#[inline(always)]
181pub unsafe fn lean_unbox_uint32(o: b_lean_obj_arg) -> u32 {
182    // SAFETY: precondition above; layout pinned by build digest.
183    unsafe { lean_ctor_get_uint32(o, 0) }
184}
185
186/// Box a `u64` as a single-field constructor (`lean.h:2835–2839`).
187///
188/// # Safety
189///
190/// Same as [`lean_box_uint32`].
191#[inline(always)]
192pub unsafe fn lean_box_uint64(v: u64) -> lean_obj_res {
193    // SAFETY: ctor allocation is unconditional; payload initialised below.
194    unsafe {
195        let o = lean_alloc_ctor(0, 0, size_of::<u64>());
196        lean_ctor_set_uint64(o, 0, v);
197        o
198    }
199}
200
201/// Recover the `u64` payload from a constructor produced by
202/// [`lean_box_uint64`] (`lean.h:2841–2843`).
203///
204/// # Safety
205///
206/// `o` must be a borrowed constructor object produced by
207/// [`lean_box_uint64`] (or by Lean's compiler in a polymorphic position
208/// holding a `UInt64`).
209#[inline(always)]
210pub unsafe fn lean_unbox_uint64(o: b_lean_obj_arg) -> u64 {
211    // SAFETY: precondition above.
212    unsafe { lean_ctor_get_uint64(o, 0) }
213}
214
215/// Box a `usize` as a single-field constructor (`lean.h:2845–2849`).
216///
217/// # Safety
218///
219/// Same as [`lean_box_uint32`].
220#[inline(always)]
221pub unsafe fn lean_box_usize(v: usize) -> lean_obj_res {
222    // SAFETY: ctor allocation is unconditional; payload initialised below.
223    unsafe {
224        let o = lean_alloc_ctor(0, 0, size_of::<usize>());
225        lean_ctor_set_usize(o, 0, v);
226        o
227    }
228}
229
230/// Recover the `usize` payload from a constructor produced by
231/// [`lean_box_usize`] (`lean.h:2851–2853`).
232///
233/// # Safety
234///
235/// `o` must be a borrowed constructor object produced by
236/// [`lean_box_usize`] (or by Lean's compiler in a polymorphic position
237/// holding a `USize`).
238#[inline(always)]
239pub unsafe fn lean_unbox_usize(o: b_lean_obj_arg) -> usize {
240    // SAFETY: precondition above.
241    unsafe { lean_ctor_get_usize(o, 0) }
242}
243
244/// Box an `f64` as a single-field constructor (`lean.h:2855–2859`).
245///
246/// # Safety
247///
248/// Same as [`lean_box_uint32`].
249#[inline(always)]
250pub unsafe fn lean_box_float(v: f64) -> lean_obj_res {
251    // SAFETY: ctor allocation is unconditional; payload initialised below.
252    unsafe {
253        let o = lean_alloc_ctor(0, 0, size_of::<f64>());
254        lean_ctor_set_float(o, 0, v);
255        o
256    }
257}
258
259/// Recover the `f64` payload from a constructor produced by
260/// [`lean_box_float`] (`lean.h:2861–2863`).
261///
262/// # Safety
263///
264/// `o` must be a borrowed constructor object produced by [`lean_box_float`]
265/// (or by Lean's compiler in a polymorphic position holding a `Float`).
266#[inline(always)]
267pub unsafe fn lean_unbox_float(o: b_lean_obj_arg) -> f64 {
268    // SAFETY: precondition above.
269    unsafe { lean_ctor_get_float(o, 0) }
270}
271
272/// Read a `usize` field stored after the object-pointer slots (`lean.h:692`).
273///
274/// # Safety
275///
276/// `o` must be a borrowed constructor object whose scalar payload includes
277/// at least one `usize` at slot `i` (counted in `usize` units past the
278/// object-pointer fields).
279#[inline(always)]
280pub unsafe fn lean_ctor_get_usize(o: b_lean_obj_arg, i: u8) -> usize {
281    // SAFETY: precondition above. The scalar area starts at the first
282    // object-slot pointer, which is `*mut lean_object`-aligned (8 bytes on
283    // 64-bit, 4 on 32-bit)—sufficient for `usize`. `read_unaligned` is
284    // used to mirror C's byte-pointer cast without invoking Rust's
285    // strict-alignment requirement on plain pointer reads.
286    unsafe { lean_ctor_obj_cptr(o).add(i as usize).cast::<usize>().read_unaligned() }
287}
288
289/// Read a `u8` field at byte `offset` within the scalar payload
290/// (`lean.h:697–700`).
291///
292/// # Safety
293///
294/// `o` must be a borrowed constructor object whose scalar payload extends
295/// at least `offset + 1` bytes past the object-pointer slots.
296#[inline(always)]
297pub unsafe fn lean_ctor_get_uint8(o: b_lean_obj_arg, offset: u32) -> u8 {
298    // SAFETY: precondition above; mirrors C's pointer arithmetic verbatim.
299    unsafe { *lean_ctor_scalar_cptr(o).add(offset as usize) }
300}
301
302/// Read a `u16` field at byte `offset` within the scalar payload
303/// (`lean.h:702–705`).
304///
305/// # Safety
306///
307/// Same as [`lean_ctor_get_uint8`]; the caller is expected to use a
308/// naturally aligned `offset`, but `read_unaligned` makes the call sound
309/// even if alignment is off.
310#[inline(always)]
311pub unsafe fn lean_ctor_get_uint16(o: b_lean_obj_arg, offset: u32) -> u16 {
312    // SAFETY: precondition above.
313    unsafe {
314        lean_ctor_scalar_cptr(o)
315            .add(offset as usize)
316            .cast::<u16>()
317            .read_unaligned()
318    }
319}
320
321/// Read a `u32` field at byte `offset` within the scalar payload
322/// (`lean.h:707–710`).
323///
324/// # Safety
325///
326/// Same as [`lean_ctor_get_uint16`].
327#[inline(always)]
328pub unsafe fn lean_ctor_get_uint32(o: b_lean_obj_arg, offset: u32) -> u32 {
329    // SAFETY: precondition above.
330    unsafe {
331        lean_ctor_scalar_cptr(o)
332            .add(offset as usize)
333            .cast::<u32>()
334            .read_unaligned()
335    }
336}
337
338/// Read a `u64` field at byte `offset` within the scalar payload
339/// (`lean.h:712–715`).
340///
341/// # Safety
342///
343/// Same as [`lean_ctor_get_uint16`].
344#[inline(always)]
345pub unsafe fn lean_ctor_get_uint64(o: b_lean_obj_arg, offset: u32) -> u64 {
346    // SAFETY: precondition above.
347    unsafe {
348        lean_ctor_scalar_cptr(o)
349            .add(offset as usize)
350            .cast::<u64>()
351            .read_unaligned()
352    }
353}
354
355/// Read an `f64` field at byte `offset` within the scalar payload
356/// (`lean.h:717–720`).
357///
358/// # Safety
359///
360/// Same as [`lean_ctor_get_uint64`].
361#[inline(always)]
362pub unsafe fn lean_ctor_get_float(o: b_lean_obj_arg, offset: u32) -> f64 {
363    // SAFETY: precondition above.
364    unsafe {
365        lean_ctor_scalar_cptr(o)
366            .add(offset as usize)
367            .cast::<f64>()
368            .read_unaligned()
369    }
370}
371
372/// Write a `usize` field at slot `i` (counted in `usize` units past the
373/// object-pointer slots)—mirror of `lean.h:727–730`.
374///
375/// # Safety
376///
377/// `o` must be a borrowed Lean constructor object whose scalar payload
378/// includes at least one `usize` at slot `i`.
379#[inline(always)]
380pub unsafe fn lean_ctor_set_usize(o: b_lean_obj_arg, i: u8, v: usize) {
381    // SAFETY: precondition above; pointer-aligned write through
382    // `write_unaligned` to match the read-side helper.
383    unsafe { lean_ctor_obj_cptr(o).add(i as usize).cast::<usize>().write_unaligned(v) }
384}
385
386/// Write a `u8` field at byte `offset` within the scalar payload
387/// (`lean.h:732–735`).
388///
389/// # Safety
390///
391/// `o` must be a borrowed Lean constructor object whose scalar payload
392/// extends at least `offset + 1` bytes past the object-pointer slots.
393#[inline(always)]
394pub unsafe fn lean_ctor_set_uint8(o: b_lean_obj_arg, offset: u32, v: u8) {
395    // SAFETY: precondition above.
396    unsafe { *lean_ctor_scalar_cptr(o).add(offset as usize) = v }
397}
398
399/// Write a `u16` field at byte `offset` within the scalar payload
400/// (`lean.h:737–740`).
401///
402/// # Safety
403///
404/// Same as [`lean_ctor_set_uint8`].
405#[inline(always)]
406pub unsafe fn lean_ctor_set_uint16(o: b_lean_obj_arg, offset: u32, v: u16) {
407    // SAFETY: precondition above.
408    unsafe {
409        lean_ctor_scalar_cptr(o)
410            .add(offset as usize)
411            .cast::<u16>()
412            .write_unaligned(v);
413    }
414}
415
416/// Write a `u32` field at byte `offset` within the scalar payload
417/// (`lean.h:742–745`).
418///
419/// # Safety
420///
421/// Same as [`lean_ctor_set_uint16`].
422#[inline(always)]
423pub unsafe fn lean_ctor_set_uint32(o: b_lean_obj_arg, offset: u32, v: u32) {
424    // SAFETY: precondition above.
425    unsafe {
426        lean_ctor_scalar_cptr(o)
427            .add(offset as usize)
428            .cast::<u32>()
429            .write_unaligned(v);
430    }
431}
432
433/// Write a `u64` field at byte `offset` within the scalar payload
434/// (`lean.h:747–750`).
435///
436/// # Safety
437///
438/// Same as [`lean_ctor_set_uint16`].
439#[inline(always)]
440pub unsafe fn lean_ctor_set_uint64(o: b_lean_obj_arg, offset: u32, v: u64) {
441    // SAFETY: precondition above.
442    unsafe {
443        lean_ctor_scalar_cptr(o)
444            .add(offset as usize)
445            .cast::<u64>()
446            .write_unaligned(v);
447    }
448}
449
450/// Write an `f64` field at byte `offset` within the scalar payload
451/// (`lean.h:752–755`).
452///
453/// # Safety
454///
455/// Same as [`lean_ctor_set_uint64`].
456#[inline(always)]
457pub unsafe fn lean_ctor_set_float(o: b_lean_obj_arg, offset: u32, v: f64) {
458    // SAFETY: precondition above.
459    unsafe {
460        lean_ctor_scalar_cptr(o)
461            .add(offset as usize)
462            .cast::<f64>()
463            .write_unaligned(v);
464    }
465}
466
467#[cfg(test)]
468mod tests {
469    //! Round-trip tests over the polymorphic-boxed scalar helpers.
470    //!
471    //! These touch real Lean allocations; they require `libleanshared` to be
472    //! discoverable at run time (the workspace's `build.rs` files bake an
473    //! rpath into the test binary).
474
475    #![allow(clippy::expect_used, clippy::float_cmp)]
476
477    use super::{lean_box_float, lean_box_uint32, lean_box_uint64, lean_box_usize};
478    use super::{lean_unbox_float, lean_unbox_uint32, lean_unbox_uint64, lean_unbox_usize};
479    use crate::init::{lean_initialize, lean_initialize_runtime_module};
480    use crate::io::lean_io_mark_end_initialization;
481    use crate::object::lean_box;
482    use crate::refcount::lean_dec;
483
484    /// Bring the Lean runtime up exactly once for this crate's tests. The
485    /// safe `LeanRuntime` lives in `lean-rs`, which is downstream; here we
486    /// open-code the same one-shot pattern with a local `OnceLock` so this
487    /// crate's tests stay self-contained.
488    fn ensure_runtime() {
489        use std::sync::OnceLock;
490        static INIT: OnceLock<()> = OnceLock::new();
491        INIT.get_or_init(|| {
492            // SAFETY: standard Lean init sequence (`lean.h` "How to use").
493            unsafe {
494                lean_initialize_runtime_module();
495                lean_initialize();
496                lean_io_mark_end_initialization();
497            }
498        });
499    }
500
501    #[test]
502    #[cfg_attr(miri, ignore = "executes libleanshared; Miri cannot interpret the Lean C runtime")]
503    fn box_unbox_uint64_round_trips() {
504        ensure_runtime();
505        for v in [0_u64, 1, u64::from(u32::MAX), u64::MAX] {
506            // SAFETY: `lean_box_uint64` produces an owned ctor; we read it
507            // back through `lean_unbox_uint64` and release via `lean_dec`.
508            unsafe {
509                let o = lean_box_uint64(v);
510                assert_eq!(lean_unbox_uint64(o), v);
511                lean_dec(o);
512            }
513        }
514    }
515
516    #[test]
517    #[cfg_attr(miri, ignore = "executes libleanshared; Miri cannot interpret the Lean C runtime")]
518    fn box_unbox_usize_round_trips() {
519        ensure_runtime();
520        for v in [0_usize, 1, usize::MAX] {
521            // SAFETY: same ownership contract as `box_unbox_uint64_round_trips`.
522            unsafe {
523                let o = lean_box_usize(v);
524                assert_eq!(lean_unbox_usize(o), v);
525                lean_dec(o);
526            }
527        }
528    }
529
530    #[test]
531    #[cfg_attr(miri, ignore = "executes libleanshared; Miri cannot interpret the Lean C runtime")]
532    fn box_unbox_uint32_round_trips() {
533        ensure_runtime();
534        for v in [0_u32, 1, u32::MAX] {
535            // SAFETY: same ownership contract as `box_unbox_uint64_round_trips`.
536            unsafe {
537                let o = lean_box_uint32(v);
538                assert_eq!(lean_unbox_uint32(o), v);
539                lean_dec(o);
540            }
541        }
542    }
543
544    #[test]
545    #[cfg_attr(miri, ignore = "executes libleanshared; Miri cannot interpret the Lean C runtime")]
546    fn box_unbox_float_round_trips() {
547        ensure_runtime();
548        for v in [0.0_f64, -1.5, core::f64::consts::PI, f64::INFINITY] {
549            // SAFETY: same ownership contract as `box_unbox_uint64_round_trips`.
550            unsafe {
551                let o = lean_box_float(v);
552                assert_eq!(lean_unbox_float(o), v);
553                lean_dec(o);
554            }
555        }
556        // NaN is a separate assertion: `==` is false against itself.
557        // SAFETY: same ownership contract as above.
558        unsafe {
559            let o = lean_box_float(f64::NAN);
560            assert!(lean_unbox_float(o).is_nan());
561            lean_dec(o);
562        }
563    }
564
565    #[test]
566    #[cfg_attr(miri, ignore = "executes libleanshared; Miri cannot interpret the Lean C runtime")]
567    fn alloc_sarray_round_trips_payload_bytes() {
568        ensure_runtime();
569        use crate::array::{
570            lean_alloc_sarray, lean_sarray_capacity, lean_sarray_cptr, lean_sarray_elem_size, lean_sarray_size,
571        };
572
573        let bytes: &[u8] = b"hello\0world";
574        // SAFETY: allocate a one-byte-element sarray sized to `bytes`, copy
575        // into the storage, then read the header + payload back out.
576        unsafe {
577            let o = lean_alloc_sarray(1, bytes.len(), bytes.len());
578            core::ptr::copy_nonoverlapping(bytes.as_ptr(), lean_sarray_cptr(o), bytes.len());
579
580            assert_eq!(lean_sarray_elem_size(o), 1);
581            assert_eq!(lean_sarray_size(o), bytes.len());
582            assert_eq!(lean_sarray_capacity(o), bytes.len());
583
584            let view = core::slice::from_raw_parts(lean_sarray_cptr(o), lean_sarray_size(o));
585            assert_eq!(view, bytes);
586
587            lean_dec(o);
588        }
589    }
590
591    #[test]
592    #[cfg_attr(miri, ignore = "executes libleanshared; Miri cannot interpret the Lean C runtime")]
593    fn alloc_sarray_empty_is_valid() {
594        ensure_runtime();
595        use crate::array::{lean_alloc_sarray, lean_sarray_size};
596        // SAFETY: zero-length sarray; allocation succeeds and size reads back
597        // as zero.
598        unsafe {
599            let o = lean_alloc_sarray(1, 0, 0);
600            assert_eq!(lean_sarray_size(o), 0);
601            lean_dec(o);
602        }
603    }
604
605    #[test]
606    #[cfg_attr(miri, ignore = "executes libleanshared; Miri cannot interpret the Lean C runtime")]
607    fn alloc_array_round_trips_object_slots() {
608        ensure_runtime();
609        use crate::array::{
610            lean_alloc_array, lean_array_capacity, lean_array_get_core, lean_array_set_core, lean_array_size,
611        };
612        use crate::object::{lean_box, lean_is_array, lean_unbox};
613
614        // SAFETY: build an object array of three scalar elements, read each
615        // slot back via `lean_array_get_core`, then release. Scalar
616        // elements skip refcount churn so the test isolates the array
617        // allocator and slot-write path.
618        unsafe {
619            let o = lean_alloc_array(3, 3);
620            assert!(lean_is_array(o));
621            assert_eq!(lean_array_size(o), 3);
622            assert_eq!(lean_array_capacity(o), 3);
623
624            lean_array_set_core(o, 0, lean_box(10));
625            lean_array_set_core(o, 1, lean_box(20));
626            lean_array_set_core(o, 2, lean_box(30));
627
628            assert_eq!(lean_unbox(lean_array_get_core(o, 0)), 10);
629            assert_eq!(lean_unbox(lean_array_get_core(o, 1)), 20);
630            assert_eq!(lean_unbox(lean_array_get_core(o, 2)), 30);
631
632            lean_dec(o);
633        }
634    }
635
636    #[test]
637    #[cfg_attr(miri, ignore = "executes libleanshared; Miri cannot interpret the Lean C runtime")]
638    fn alloc_array_empty_is_valid() {
639        ensure_runtime();
640        use crate::array::{lean_alloc_array, lean_array_capacity, lean_array_size};
641        use crate::object::lean_is_array;
642
643        // SAFETY: zero-length object array; allocation succeeds and the
644        // size/capacity header reads back as zero. No element slots to
645        // initialise.
646        unsafe {
647            let o = lean_alloc_array(0, 0);
648            assert!(lean_is_array(o));
649            assert_eq!(lean_array_size(o), 0);
650            assert_eq!(lean_array_capacity(o), 0);
651            lean_dec(o);
652        }
653    }
654
655    #[test]
656    #[cfg_attr(miri, ignore = "executes libleanshared; Miri cannot interpret the Lean C runtime")]
657    fn scalar_box_unbox_remains_inline_for_small_nat() {
658        // Sanity: the existing scalar `lean_box` / `lean_unbox` from
659        // `crate::object` is a distinct path that must not interact with
660        // the ctor-box helpers added here.
661        // SAFETY: scalar-tagged pointer arithmetic only.
662        unsafe {
663            let o = lean_box(42);
664            assert_eq!(crate::object::lean_unbox(o), 42);
665            lean_dec(o);
666        }
667    }
668}