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}