#![allow(unsafe_code)]
use core::mem::MaybeUninit;
use lean_rs_sys::ctor::{
lean_alloc_ctor, lean_ctor_get_uint8, lean_ctor_get_uint64, lean_ctor_num_objs, lean_ctor_obj_cptr,
lean_ctor_scalar_cptr,
};
use lean_rs_sys::object::{lean_is_ctor, lean_is_scalar, lean_obj_tag, lean_object_data_byte_size};
use lean_rs_sys::refcount::lean_inc;
use crate::abi::traits::conversion_error;
use crate::error::LeanResult;
use crate::runtime::LeanRuntime;
use crate::runtime::obj::Obj;
pub struct ObjView<'lean, 'a> {
obj: &'a Obj<'lean>,
}
#[derive(Copy, Clone)]
pub struct CtorView<'lean, 'a> {
obj: &'a Obj<'lean>,
tag: u8,
num_object_fields: usize,
scalar_tail_size: usize,
}
#[inline]
#[must_use]
pub fn view<'lean, 'a>(obj: &'a Obj<'lean>) -> ObjView<'lean, 'a> {
ObjView { obj }
}
impl<'lean, 'a> ObjView<'lean, 'a> {
#[inline]
#[must_use]
pub fn is_scalar(&self) -> bool {
let ptr = self.obj.as_raw_borrowed();
unsafe { lean_is_scalar(ptr) }
}
#[inline]
pub fn scalar_payload(&self, label: &str) -> LeanResult<usize> {
let ptr = self.obj.as_raw_borrowed();
if unsafe { lean_is_scalar(ptr) } {
Ok(unsafe { lean_rs_sys::object::lean_unbox(ptr) })
} else {
let found_tag = unsafe { lean_obj_tag(ptr) };
Err(conversion_error(format!(
"expected Lean {label} scalar-tagged object, found heap object with tag {found_tag}"
)))
}
}
#[inline]
pub fn sum_tag(&self) -> LeanResult<u8> {
let ptr = self.obj.as_raw_borrowed();
if unsafe { lean_is_scalar(ptr) } {
let tag = unsafe { lean_rs_sys::object::lean_unbox(ptr) };
return u8::try_from(tag)
.map_err(|_| conversion_error(format!("Lean scalar constructor tag {tag} does not fit in u8")));
}
self.ctor().map(|ctor| ctor.tag())
}
#[inline]
pub fn ctor(&self) -> LeanResult<CtorView<'lean, 'a>> {
CtorView::new(self.obj)
}
#[inline]
pub fn ctor_shape(
&self,
expected_tag: u8,
expected_num_object_fields: usize,
label: &str,
) -> LeanResult<CtorView<'lean, 'a>> {
self.ctor()?
.require_shape(expected_tag, expected_num_object_fields, label)
}
}
impl<'lean, 'a> CtorView<'lean, 'a> {
#[inline]
fn new(obj: &'a Obj<'lean>) -> LeanResult<Self> {
let ptr = obj.as_raw_borrowed();
if unsafe { lean_is_scalar(ptr) } {
return Err(conversion_error(
"expected Lean constructor, found scalar-tagged object",
));
}
if !unsafe { lean_is_ctor(ptr) } {
let found_tag = unsafe { lean_obj_tag(ptr) };
return Err(conversion_error(format!(
"expected Lean constructor, found object with tag {found_tag}"
)));
}
let tag = unsafe { lean_obj_tag(ptr) };
let num_object_fields = unsafe { lean_ctor_num_objs(ptr) } as usize;
#[allow(
clippy::cast_possible_truncation,
reason = "ctor tag is bounded by LEAN_MAX_CTOR_TAG"
)]
let tag = tag as u8;
let scalar_tail_size = ctor_scalar_tail_size(ptr);
Ok(Self {
obj,
tag,
num_object_fields,
scalar_tail_size,
})
}
#[inline]
#[must_use]
pub fn tag(&self) -> u8 {
self.tag
}
#[inline]
fn require_tag(self, expected_tag: u8, label: &str) -> LeanResult<Self> {
if self.tag == expected_tag {
Ok(self)
} else {
Err(conversion_error(format!(
"expected Lean {label} ctor (tag {expected_tag}), found tag {}",
self.tag
)))
}
}
#[inline]
fn require_shape(self, expected_tag: u8, expected_num_object_fields: usize, label: &str) -> LeanResult<Self> {
let this = self.require_tag(expected_tag, label)?;
if this.num_object_fields == expected_num_object_fields {
Ok(this)
} else {
Err(conversion_error(format!(
"expected Lean {label} ctor with {expected_num_object_fields} object field(s), found {}",
this.num_object_fields
)))
}
}
#[inline]
pub fn uint8(&self, offset: u32, label: &str) -> LeanResult<u8> {
self.require_scalar_tail(offset, 1, label)?;
let ptr = self.obj.as_raw_borrowed();
Ok(unsafe { lean_ctor_get_uint8(ptr, offset) })
}
#[inline]
pub fn uint64(&self, offset: u32, label: &str) -> LeanResult<u64> {
self.require_scalar_tail(offset, core::mem::size_of::<u64>(), label)?;
let ptr = self.obj.as_raw_borrowed();
Ok(unsafe { lean_ctor_get_uint64(ptr, offset) })
}
#[inline]
pub fn bool(&self, offset: u32, label: &str) -> LeanResult<bool> {
match self.uint8(offset, label)? {
0 => Ok(false),
1 => Ok(true),
other => Err(conversion_error(format!(
"Lean {label} byte {other} is not in {{0, 1}}"
))),
}
}
#[inline]
fn require_scalar_tail(&self, offset: u32, width: usize, label: &str) -> LeanResult<()> {
let start = offset as usize;
let Some(end) = start.checked_add(width) else {
return Err(conversion_error(format!(
"Lean {label} scalar-tail read at offset {offset} overflows usize"
)));
};
if end <= self.scalar_tail_size {
Ok(())
} else {
Err(conversion_error(format!(
"Lean {label} scalar-tail read at offset {offset} for {width} byte(s) exceeds scalar tail size {}",
self.scalar_tail_size
)))
}
}
}
pub fn alloc_ctor_with_objects<'lean, const N: usize>(
runtime: &'lean LeanRuntime,
tag: u8,
objects: [Obj<'lean>; N],
) -> Obj<'lean> {
unsafe {
const { assert!(N <= u8::MAX as usize, "ctor arity exceeds Lean's u8 num_objs field") };
let raw = lean_alloc_ctor(tag, N as u8, 0);
let slots = lean_ctor_obj_cptr(raw);
for (i, field) in objects.into_iter().enumerate() {
*slots.add(i) = field.into_raw();
}
Obj::from_owned_raw(runtime, raw)
}
}
pub fn take_ctor_objects<'lean, const N: usize>(
obj: Obj<'lean>,
expected_tag: u8,
label: &str,
) -> LeanResult<[Obj<'lean>; N]> {
require_ctor_shape(&obj, expected_tag, N, label)?;
let runtime = obj.runtime();
let ptr = obj.as_raw_borrowed();
let slots = unsafe { lean_ctor_obj_cptr(ptr) };
let mut out: [MaybeUninit<Obj<'lean>>; N] = [const { MaybeUninit::uninit() }; N];
for (i, slot) in out.iter_mut().enumerate() {
unsafe {
let field_ptr = *slots.add(i);
lean_inc(field_ptr);
slot.write(Obj::from_owned_raw(runtime, field_ptr));
}
}
drop(obj);
Ok(out.map(|cell| unsafe { cell.assume_init() }))
}
pub fn ctor_tag(obj: &Obj<'_>) -> LeanResult<u8> {
view(obj).ctor().map(|ctor| ctor.tag())
}
fn require_ctor_shape(obj: &Obj<'_>, expected_tag: u8, expected_num_objs: usize, label: &str) -> LeanResult<()> {
let _ = view(obj).ctor_shape(expected_tag, expected_num_objs, label)?;
Ok(())
}
#[inline]
fn ctor_scalar_tail_size(ptr: *mut lean_rs_sys::lean_object) -> usize {
unsafe {
let scalar_start = lean_ctor_scalar_cptr(ptr) as usize;
let object_start = ptr as usize;
let scalar_offset = scalar_start.saturating_sub(object_start);
lean_object_data_byte_size(ptr).saturating_sub(scalar_offset)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use core::ffi::c_char;
use lean_rs_sys::ctor::{lean_alloc_ctor, lean_ctor_set_uint8, lean_ctor_set_uint64};
use lean_rs_sys::object::{lean_box, lean_is_exclusive, lean_is_shared};
use lean_rs_sys::string::lean_mk_string;
use super::{alloc_ctor_with_objects, take_ctor_objects, view};
use crate::runtime::LeanRuntime;
use crate::runtime::obj::Obj;
fn runtime() -> &'static LeanRuntime {
LeanRuntime::init().expect("runtime init must succeed")
}
fn scalar_obj(runtime: &LeanRuntime, payload: usize) -> Obj<'_> {
unsafe { Obj::from_owned_raw(runtime, lean_box(payload)) }
}
fn heap_string(runtime: &LeanRuntime) -> Obj<'_> {
let cstr = c"field".as_ptr().cast::<c_char>();
unsafe { Obj::from_owned_raw(runtime, lean_mk_string(cstr)) }
}
fn ctor_with_scalar_tail(runtime: &LeanRuntime) -> Obj<'_> {
unsafe {
let raw = lean_alloc_ctor(2, 0, 16);
lean_ctor_set_uint8(raw, 0, 1);
lean_ctor_set_uint64(raw, 8, 0x0102_0304_0506_0708);
Obj::from_owned_raw(runtime, raw)
}
}
#[test]
fn view_discriminates_scalar_and_constructor() {
let runtime = runtime();
let scalar = scalar_obj(runtime, 3);
assert!(view(&scalar).is_scalar());
assert_eq!(view(&scalar).scalar_payload("TestScalar").expect("scalar payload"), 3);
assert_eq!(view(&scalar).sum_tag().expect("scalar sum tag"), 3);
let ctor = alloc_ctor_with_objects(runtime, 1, []);
let ctor_view = view(&ctor).ctor().expect("ctor view");
assert!(!view(&ctor).is_scalar());
assert_eq!(ctor_view.tag(), 1);
}
#[test]
fn constructor_shape_checks_tag_and_object_field_count() {
let runtime = runtime();
let ctor = alloc_ctor_with_objects(runtime, 4, [scalar_obj(runtime, 9)]);
let ctor_view = view(&ctor).ctor_shape(4, 1, "OneField").expect("expected ctor shape");
assert_eq!(ctor_view.tag(), 4);
assert!(view(&ctor).ctor_shape(5, 1, "OneField").is_err());
assert!(view(&ctor).ctor_shape(4, 2, "OneField").is_err());
}
#[test]
fn scalar_tail_reads_are_bounds_checked() {
let runtime = runtime();
let ctor = ctor_with_scalar_tail(runtime);
let ctor_view = view(&ctor).ctor_shape(2, 0, "ScalarTail").expect("expected ctor shape");
assert!(ctor_view.bool(0, "ScalarTail.flag").expect("bool tail"));
assert_eq!(
ctor_view.uint64(8, "ScalarTail.count").expect("u64 tail"),
0x0102_0304_0506_0708,
);
assert!(ctor_view.uint64(9, "ScalarTail.count").is_err());
assert!(ctor_view.uint8(16, "ScalarTail.flag").is_err());
}
#[test]
fn malformed_object_shape_errors_without_panicking() {
let runtime = runtime();
let scalar = scalar_obj(runtime, 0);
assert!(view(&scalar).ctor().is_err());
let wide_scalar = scalar_obj(runtime, usize::from(u8::MAX) + 1);
assert!(view(&wide_scalar).sum_tag().is_err());
let ctor = ctor_with_scalar_tail(runtime);
assert!(view(&ctor).scalar_payload("ExpectedScalar").is_err());
}
#[test]
fn take_ctor_objects_preserves_field_ownership() {
let runtime = runtime();
let child = heap_string(runtime);
let witness = child.clone();
let parent = alloc_ctor_with_objects(runtime, 0, [child]);
let [taken] = take_ctor_objects::<1>(parent, 0, "Parent").expect("take field");
assert!(unsafe { lean_is_shared(taken.as_raw_borrowed()) });
assert!(unsafe { lean_is_shared(witness.as_raw_borrowed()) });
drop(taken);
assert!(unsafe { lean_is_exclusive(witness.as_raw_borrowed()) });
drop(witness);
}
}