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//! - [`ctor_tag`] is a borrow-only read; it never touches the refcount.
33
34// SAFETY DOC: every `unsafe { ... }` block in this file carries its own
35// `// SAFETY:` comment naming the invariant; the blanket allow keeps the
36// unsafe surface inside the smallest scope that compiles, per
37// `docs/architecture/01-safety-model.md`.
38#![allow(unsafe_code)]
39
40use core::mem::MaybeUninit;
41
42use lean_rs_sys::ctor::{lean_alloc_ctor, lean_ctor_num_objs, lean_ctor_obj_cptr};
43use lean_rs_sys::object::{lean_is_ctor, lean_is_scalar, lean_obj_tag};
44use lean_rs_sys::refcount::lean_inc;
45
46use crate::abi::traits::conversion_error;
47use crate::error::LeanResult;
48use crate::runtime::LeanRuntime;
49use crate::runtime::obj::Obj;
50
51/// Allocate a freshly-initialised constructor with `N` object-pointer
52/// fields and no scalar payload.
53///
54/// `tag` is the inductive constructor index (Lean's declaration order:
55/// `Option.none` = 0, `Option.some` = 1, `Except.error` = 0, `Except.ok`
56/// = 1, …). Each entry of `objects` is moved into its slot via
57/// [`Obj::into_raw`], so the returned [`Obj`] owns the only live refcount
58/// per field plus its own header count. The const-generic `N` matches the
59/// number of object-pointer slots the Lean inductive declares, which
60/// keeps the call site self-documenting and lets the compiler refuse
61/// arity mismatches.
62///
63/// # Panics
64///
65/// Panics only via `lean_alloc_ctor`'s `strict_*` arithmetic overflow
66/// guard — unreachable for the constructor shapes Lean emits
67/// (`LEAN_MAX_CTOR_FIELDS` = 256).
68pub fn alloc_ctor_with_objects<'lean, const N: usize>(
69    runtime: &'lean LeanRuntime,
70    tag: u8,
71    objects: [Obj<'lean>; N],
72) -> Obj<'lean> {
73    // SAFETY: `lean_alloc_ctor` returns a fresh ctor with refcount 1 and
74    // `N` uninitialised object slots; we write each `Obj::into_raw` into
75    // its slot before the object escapes, satisfying the
76    // "fully initialise every declared field" obligation.
77    unsafe {
78        // The `num_objs` parameter is `u8`; assert at compile time that
79        // `N` fits, matching `lean.h`'s `LEAN_MAX_CTOR_FIELDS` ceiling
80        // (the const block evaluates at type-checking time).
81        const { assert!(N <= u8::MAX as usize, "ctor arity exceeds Lean's u8 num_objs field") };
82        let raw = lean_alloc_ctor(tag, N as u8, 0);
83        let slots = lean_ctor_obj_cptr(raw);
84        for (i, field) in objects.into_iter().enumerate() {
85            *slots.add(i) = field.into_raw();
86        }
87        Obj::from_owned_raw(runtime, raw)
88    }
89}
90
91/// Validate that `obj` is a constructor with `expected_tag` and exactly
92/// `N` object-pointer fields, then return the `N` owned field handles.
93///
94/// Each returned [`Obj`] carries one refcount: [`lean_inc`] is called on
95/// the slot pointer before wrapping it. The parent `obj` is consumed and
96/// its [`Drop`] runs the matching `lean_dec` (which decrements each field
97/// once more — balancing the `lean_inc`s and leaving the returned handles
98/// with the same effective ownership the parent originally held).
99///
100/// `label` is embedded in the diagnostic on failure so callers see
101/// `"expected Lean Option::some ctor (tag 1, num_objs 1), …"` rather
102/// than an anonymous "wrong ctor".
103///
104/// # Errors
105///
106/// Returns [`HostStage::Conversion`](crate::error::HostStage::Conversion)
107/// if `obj` is scalar-tagged, has a non-constructor heap tag, has a
108/// different tag from `expected_tag`, or carries a different
109/// object-slot count from `N`.
110pub fn take_ctor_objects<'lean, const N: usize>(
111    obj: Obj<'lean>,
112    expected_tag: u8,
113    label: &str,
114) -> LeanResult<[Obj<'lean>; N]> {
115    require_ctor_shape(&obj, expected_tag, N, label)?;
116    let runtime = obj.runtime();
117    let ptr = obj.as_raw_borrowed();
118    // SAFETY: shape validated above; `lean_ctor_obj_cptr` returns a
119    // pointer valid for `N` slots, each holding a live owned
120    // `*mut lean_object` (well-formed Lean ctor invariant).
121    let slots = unsafe { lean_ctor_obj_cptr(ptr) };
122    let mut out: [MaybeUninit<Obj<'lean>>; N] = [const { MaybeUninit::uninit() }; N];
123    for (i, slot) in out.iter_mut().enumerate() {
124        // SAFETY: index in `0..N` is in-bounds per the shape check; the
125        // slot read is a borrowed view, then `lean_inc` bumps the refcount
126        // so the wrapped `Obj` owns its own count independent of the
127        // parent.
128        unsafe {
129            let field_ptr = *slots.add(i);
130            lean_inc(field_ptr);
131            slot.write(Obj::from_owned_raw(runtime, field_ptr));
132        }
133    }
134    // The parent `obj` falls out of scope here; its `Drop` releases the
135    // original constructor (and the per-field counts the parent held).
136    drop(obj);
137    // SAFETY: every element of `out` was initialised in the loop above
138    // (`0..N` covers the whole array exactly once); transmute is sound
139    // because `[MaybeUninit<T>; N]` and `[T; N]` share layout.
140    Ok(out.map(|cell| unsafe { cell.assume_init() }))
141}
142
143/// Read the tag byte of a constructor object.
144///
145/// Used by sum-type decoders (`Option` and `except::Except` (sibling-module sum-type carriers))
146/// that need to pick a variant before they know the arity. Borrow-only:
147/// leaves the refcount untouched.
148///
149/// # Errors
150///
151/// Returns [`HostStage::Conversion`](crate::error::HostStage::Conversion)
152/// if `obj` is not a heap-allocated constructor.
153pub fn ctor_tag(obj: &Obj<'_>) -> LeanResult<u8> {
154    let ptr = obj.as_raw_borrowed();
155    // SAFETY: `lean_is_scalar` is pure pointer-bit math.
156    if unsafe { lean_is_scalar(ptr) } {
157        return Err(conversion_error(
158            "expected Lean constructor, found scalar-tagged object",
159        ));
160    }
161    // SAFETY: non-scalar; tag read on the owned object header.
162    if !unsafe { lean_is_ctor(ptr) } {
163        // SAFETY: same branch.
164        let found_tag = unsafe { lean_obj_tag(ptr) };
165        return Err(conversion_error(format!(
166            "expected Lean constructor, found object with tag {found_tag}"
167        )));
168    }
169    // SAFETY: ctor object — its tag fits a `u8` per `lean.h`'s
170    // `LEAN_MAX_CTOR_TAG` ceiling.
171    let tag = unsafe { lean_obj_tag(ptr) };
172    // Logical ctor tags are bounded by `LEAN_MAX_CTOR_TAG` (=243), so the
173    // cast is lossless inside the gate.
174    #[allow(
175        clippy::cast_possible_truncation,
176        reason = "ctor tag is bounded by LEAN_MAX_CTOR_TAG"
177    )]
178    Ok(tag as u8)
179}
180
181/// Shared validator for [`take_ctor_objects`]: ctor kind, matching tag,
182/// matching `num_objs`.
183fn require_ctor_shape(obj: &Obj<'_>, expected_tag: u8, expected_num_objs: usize, label: &str) -> LeanResult<()> {
184    let found_tag = ctor_tag(obj)?;
185    if found_tag != expected_tag {
186        return Err(conversion_error(format!(
187            "expected Lean {label} ctor (tag {expected_tag}), found tag {found_tag}"
188        )));
189    }
190    let ptr = obj.as_raw_borrowed();
191    // SAFETY: `ctor_tag` already validated `obj` is a constructor; the
192    // `m_other` field then holds its num_objs count.
193    let found_num_objs = unsafe { lean_ctor_num_objs(ptr) } as usize;
194    if found_num_objs != expected_num_objs {
195        return Err(conversion_error(format!(
196            "expected Lean {label} ctor with {expected_num_objs} object field(s), found {found_num_objs}"
197        )));
198    }
199    Ok(())
200}