#![allow(unsafe_code)]
#![allow(
dead_code,
reason = "decoder helpers reached through generic dispatch; lib-only build cannot prove reachability"
)]
use core::slice;
use lean_rs_sys::ctor::{lean_ctor_num_objs, lean_ctor_obj_cptr};
use lean_rs_sys::io::{lean_io_result_get_error, lean_io_result_is_ok, lean_io_result_take_value};
use lean_rs_sys::object::{lean_is_scalar, lean_is_string, lean_obj_tag};
use lean_rs_sys::string::{lean_string_cstr, lean_string_size};
use super::{LeanError, LeanExceptionKind, LeanResult};
use crate::runtime::LeanRuntime;
use crate::runtime::obj::Obj;
pub(crate) fn decode_io<'lean>(runtime: &'lean LeanRuntime, result: Obj<'lean>) -> LeanResult<Obj<'lean>> {
let ptr = result.as_raw_borrowed();
if unsafe { lean_io_result_is_ok(ptr) } {
let value_ptr = unsafe { lean_io_result_take_value(result.into_raw()) };
let value_obj = unsafe { Obj::from_owned_raw(runtime, value_ptr) };
Ok(value_obj)
} else {
let err_ptr = unsafe { lean_io_result_get_error(ptr) };
let (kind, message) = classify_io_error(err_ptr);
drop(result);
Err(LeanError::lean_exception(kind, message))
}
}
const KIND_TABLE: &[LeanExceptionKind] = &[
LeanExceptionKind::AlreadyExists, LeanExceptionKind::OtherError, LeanExceptionKind::ResourceBusy, LeanExceptionKind::ResourceVanished, LeanExceptionKind::UnsupportedOperation, LeanExceptionKind::HardwareFault, LeanExceptionKind::UnsatisfiedConstraints, LeanExceptionKind::IllegalOperation, LeanExceptionKind::ProtocolError, LeanExceptionKind::TimeExpired, LeanExceptionKind::Interrupted, LeanExceptionKind::NoFileOrDirectory, LeanExceptionKind::InvalidArgument, LeanExceptionKind::PermissionDenied, LeanExceptionKind::ResourceExhausted, LeanExceptionKind::InappropriateType, LeanExceptionKind::NoSuchThing, LeanExceptionKind::UnexpectedEof, LeanExceptionKind::UserError, ];
fn classify_io_error(err: *mut lean_rs_sys::lean_object) -> (LeanExceptionKind, String) {
if unsafe { lean_is_scalar(err) } {
return (
LeanExceptionKind::Other,
"<Lean IO error: scalar-encoded constructor>".to_owned(),
);
}
let ctor = unsafe { lean_obj_tag(err) };
let kind = KIND_TABLE
.get(ctor as usize)
.copied()
.unwrap_or(LeanExceptionKind::Other);
let message = read_last_string_field(err).unwrap_or_else(|| format!("<Lean IO error: constructor tag {ctor}>"));
(kind, message)
}
fn read_last_string_field(ctor: *mut lean_rs_sys::lean_object) -> Option<String> {
let n = unsafe { lean_ctor_num_objs(ctor) };
if n == 0 {
return None;
}
let fields = unsafe { lean_ctor_obj_cptr(ctor) };
let last_index = usize::from(n).saturating_sub(1);
let last = unsafe { *fields.add(last_index) };
if unsafe { lean_is_scalar(last) } {
return None;
}
if !unsafe { lean_is_string(last) } {
return None;
}
let bytes = unsafe {
let size_with_nul = lean_string_size(last);
let len = size_with_nul.saturating_sub(1);
let data = lean_string_cstr(last).cast::<u8>();
slice::from_raw_parts(data, len)
};
Some(String::from_utf8_lossy(bytes).into_owned())
}
#[cfg(test)]
mod tests {
#![allow(unsafe_code, clippy::expect_used, clippy::panic)]
use std::path::PathBuf;
use super::decode_io;
use crate::LeanRuntime;
use crate::abi::nat;
use crate::error::{LeanError, LeanExceptionKind};
use crate::module::LeanLibrary;
use crate::runtime::obj::Obj;
fn fixture_dylib_path() -> PathBuf {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let workspace = manifest_dir
.parent()
.and_then(std::path::Path::parent)
.expect("crates/<name>/ lives two directories beneath the workspace root");
let dylib_extension = if cfg!(target_os = "macos") { "dylib" } else { "so" };
let lib_dir = workspace
.join("fixtures")
.join("lean")
.join(".lake")
.join("build")
.join("lib");
let new_style = lib_dir.join(format!("liblean__rs__fixture_LeanRsFixture.{dylib_extension}"));
let old_style = lib_dir.join(format!("libLeanRsFixture.{dylib_extension}"));
if old_style.is_file() && !new_style.is_file() {
old_style
} else {
new_style
}
}
fn open_fixture(runtime: &LeanRuntime) -> LeanLibrary<'_> {
let path = fixture_dylib_path();
assert!(path.exists(), "fixture dylib not found at {}", path.display());
let library = LeanLibrary::open(runtime, &path).expect("fixture dylib opens cleanly");
drop(
library
.initialize_module("lean_rs_fixture", "LeanRsFixture")
.expect("fixture root module initializes"),
);
library
}
fn call_io_raw<'lean>(library: &LeanLibrary<'lean>, runtime: &'lean LeanRuntime, symbol: &str) -> Obj<'lean> {
let addr = library.resolve_function_symbol(symbol).expect("symbol resolves");
type FnPtr = unsafe extern "C" fn(*mut lean_rs_sys::lean_object) -> *mut lean_rs_sys::lean_object;
let f: FnPtr = unsafe { core::mem::transmute::<*mut core::ffi::c_void, FnPtr>(addr) };
let world = unsafe { lean_rs_sys::object::lean_box(0) };
let raw = unsafe { f(world) };
unsafe { Obj::from_owned_raw(runtime, raw) }
}
#[test]
fn decode_io_ok_returns_value() {
let runtime = LeanRuntime::init().expect("runtime init");
let library = open_fixture(runtime);
let result = call_io_raw(&library, runtime, "lean_rs_fixture_io_success_nat");
let value_obj = decode_io(runtime, result).expect("ioSuccessNat decodes");
let value = nat::try_to_u64(value_obj).expect("scalar Nat decodes");
assert_eq!(value, 7);
}
#[test]
fn decode_io_error_returns_lean_exception() {
let runtime = LeanRuntime::init().expect("runtime init");
let library = open_fixture(runtime);
let result = call_io_raw(&library, runtime, "lean_rs_fixture_io_throw");
match decode_io(runtime, result) {
Err(LeanError::LeanException(exc)) => {
assert_eq!(exc.kind(), LeanExceptionKind::UserError);
assert!(
exc.message().contains("deliberate IO exception"),
"unexpected message: {:?}",
exc.message()
);
}
other => panic!("expected LeanException, got {other:?}"),
}
}
#[test]
fn kind_table_length_matches_known_constructor_count() {
assert_eq!(super::KIND_TABLE.len(), 19);
}
}