1#![allow(unsafe_code)]
18
19use std::collections::HashMap;
20use std::marker::PhantomData;
21use std::num::NonZeroUsize;
22use std::rc::Rc;
23use std::slice;
24use std::sync::atomic::{AtomicUsize, Ordering};
25use std::sync::{Arc, Mutex, OnceLock};
26
27use lean_rs_sys::lean_object;
28use lean_rs_sys::object::{lean_is_scalar, lean_is_string};
29use lean_rs_sys::string::{lean_string_cstr, lean_string_size};
30
31use crate::abi::traits::{TryFromLean, conversion_error};
32use crate::error::panic::catch_callback_panic;
33use crate::error::{LeanError, LeanResult};
34use crate::runtime::obj::Obj;
35
36type ProgressCallbackFn = dyn Fn(LeanProgressTick) -> LeanCallbackFlow + Send + Sync + 'static;
37type StringCallbackFn = dyn Fn(LeanStringEvent) -> LeanCallbackFlow + Send + Sync + 'static;
38type ScopedProgressCallbackFn<'a> = dyn Fn(LeanProgressTick) -> LeanCallbackFlow + Send + Sync + 'a;
39
40const PAYLOAD_PROGRESS_TICK: u8 = 0;
41const PAYLOAD_STRING: u8 = 1;
42
43static NEXT_ID: AtomicUsize = AtomicUsize::new(1);
44static REGISTRY: OnceLock<Mutex<HashMap<usize, Arc<CallbackEntry>>>> = OnceLock::new();
45
46#[allow(private_bounds, reason = "standard sealed-trait pattern keeps payload ABI private")]
53pub trait LeanCallbackPayload: private::Sealed + Send + Sync + 'static {}
54
55#[derive(Clone, Copy, Debug, Eq, PartialEq)]
60pub struct LeanProgressTick {
61 pub current: u64,
63 pub total: u64,
65}
66
67#[derive(Clone, Debug, Eq, PartialEq)]
69pub struct LeanStringEvent {
70 pub value: String,
72}
73
74#[derive(Clone, Copy, Debug, Eq, PartialEq)]
80pub enum LeanCallbackFlow {
81 Continue,
83 Stop,
86}
87
88#[derive(Clone, Copy, Debug, Eq, PartialEq)]
93#[repr(u8)]
94pub enum LeanCallbackStatus {
95 Ok = 0,
97 StaleHandle = 1,
99 Panic = 2,
101 WrongPayload = 3,
103 Stopped = 4,
105}
106
107impl LeanCallbackStatus {
108 #[must_use]
110 pub const fn from_abi(value: u8) -> Option<Self> {
111 match value {
112 0 => Some(Self::Ok),
113 1 => Some(Self::StaleHandle),
114 2 => Some(Self::Panic),
115 3 => Some(Self::WrongPayload),
116 4 => Some(Self::Stopped),
117 _ => None,
118 }
119 }
120
121 #[must_use]
123 pub const fn as_abi(self) -> u8 {
124 self as u8
125 }
126
127 #[must_use]
129 pub const fn description(self) -> &'static str {
130 match self {
131 Self::Ok => "callback completed successfully",
132 Self::StaleHandle => "Lean called a callback handle after Rust dropped it",
133 Self::Panic => "Rust callback panicked and the trampoline contained the panic",
134 Self::WrongPayload => "Lean called a callback handle through the wrong payload trampoline",
135 Self::Stopped => "Rust callback asked Lean to stop the callback loop",
136 }
137 }
138}
139
140pub struct LeanCallbackHandle<P: LeanCallbackPayload> {
160 id: NonZeroUsize,
161 entry: Arc<CallbackEntry>,
162 _payload: PhantomData<fn(P)>,
163}
164
165pub struct LeanProgressCallback<'a> {
177 handle: LeanCallbackHandle<LeanProgressTick>,
178 #[allow(
179 dead_code,
180 reason = "keeps the borrowed callback context alive until after handle drop"
181 )]
182 context: Box<ScopedProgressContext<'a>>,
183 _not_send_or_sync: PhantomData<Rc<()>>,
184}
185
186struct ScopedProgressContext<'a> {
187 callback: Box<ScopedProgressCallbackFn<'a>>,
188}
189
190impl<P: LeanCallbackPayload> std::fmt::Debug for LeanCallbackHandle<P> {
191 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192 f.debug_struct("LeanCallbackHandle")
193 .field("id", &self.id)
194 .finish_non_exhaustive()
195 }
196}
197
198impl LeanCallbackHandle<LeanProgressTick> {
199 pub fn register<F>(callback: F) -> LeanResult<Self>
208 where
209 F: Fn(LeanProgressTick) -> LeanCallbackFlow + Send + Sync + 'static,
210 {
211 register_entry(CallbackEntry::new_progress(callback))
212 }
213}
214
215impl LeanCallbackHandle<LeanStringEvent> {
216 pub fn register<F>(callback: F) -> LeanResult<Self>
227 where
228 F: Fn(LeanStringEvent) -> LeanCallbackFlow + Send + Sync + 'static,
229 {
230 register_entry(CallbackEntry::new_string(callback))
231 }
232}
233
234impl<'a> LeanProgressCallback<'a> {
235 pub fn register<F>(callback: F) -> LeanResult<Self>
247 where
248 F: Fn(LeanProgressTick) -> LeanCallbackFlow + Send + Sync + 'a,
249 {
250 let context = Box::new(ScopedProgressContext {
251 callback: Box::new(callback),
252 });
253 let context_addr = std::ptr::from_ref(&*context).addr();
254 let handle = LeanCallbackHandle::<LeanProgressTick>::register(move |event| {
255 let context = unsafe { &*(context_addr as *const ScopedProgressContext<'_>) };
263 (context.callback)(event)
264 })?;
265 Ok(Self {
266 handle,
267 context,
268 _not_send_or_sync: PhantomData,
269 })
270 }
271
272 #[must_use]
275 pub fn abi_parts(&self) -> (usize, usize) {
276 self.handle.abi_parts()
277 }
278
279 pub fn decode_result<'lean, T>(&self, obj: Obj<'lean>) -> LeanResult<T>
293 where
294 T: TryFromLean<'lean>,
295 {
296 match Result::<T, u8>::try_from_lean(obj)? {
297 Ok(value) => Ok(value),
298 Err(status) => {
299 self.progress_status_to_result(status)?;
300 Err(LeanError::internal(
301 "progress shim returned Except.error with successful callback status",
302 ))
303 }
304 }
305 }
306
307 fn progress_status_to_result(&self, status: u8) -> LeanResult<()> {
308 match LeanCallbackStatus::from_abi(status) {
309 Some(LeanCallbackStatus::Ok) => Ok(()),
310 Some(LeanCallbackStatus::StaleHandle) => {
311 Err(LeanError::internal("Lean progress shim called a stale callback handle"))
312 }
313 Some(LeanCallbackStatus::WrongPayload) => Err(LeanError::internal(
314 "Lean progress shim called a callback handle through the wrong payload trampoline",
315 )),
316 Some(LeanCallbackStatus::Stopped) => Err(LeanError::internal(
317 "progress callback asked Lean to stop, but progress callbacks do not define stop semantics",
318 )),
319 Some(LeanCallbackStatus::Panic) => Err(self.handle.last_error().unwrap_or_else(|| {
320 LeanError::internal("progress callback panicked without recording a callback error")
321 })),
322 None => Err(conversion_error(format!(
323 "Lean progress shim returned unknown callback status byte {status}"
324 ))),
325 }
326 }
327}
328
329impl<P: LeanCallbackPayload> LeanCallbackHandle<P> {
330 #[must_use]
332 pub fn abi_handle(&self) -> usize {
333 self.id.get()
334 }
335
336 #[must_use]
342 pub fn abi_trampoline(&self) -> usize {
343 P::trampoline()
344 }
345
346 #[must_use]
349 pub fn abi_parts(&self) -> (usize, usize) {
350 (self.abi_handle(), self.abi_trampoline())
351 }
352
353 #[must_use]
359 pub fn last_error(&self) -> Option<LeanError> {
360 self.entry.last_error()
361 }
362}
363
364impl<P: LeanCallbackPayload> Drop for LeanCallbackHandle<P> {
365 fn drop(&mut self) {
366 if let Some(registry) = REGISTRY.get()
367 && let Ok(mut guard) = registry.lock()
368 {
369 drop(guard.remove(&self.id.get()));
370 }
371 }
372}
373
374enum CallbackEntryKind {
375 Progress(Box<ProgressCallbackFn>),
376 String(Box<StringCallbackFn>),
377}
378
379struct CallbackEntry {
380 kind: CallbackEntryKind,
381 last_error: Mutex<Option<LeanError>>,
382}
383
384impl std::fmt::Debug for CallbackEntry {
385 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386 f.debug_struct("CallbackEntry").finish_non_exhaustive()
387 }
388}
389
390impl CallbackEntry {
391 fn new_progress<F>(callback: F) -> Self
392 where
393 F: Fn(LeanProgressTick) -> LeanCallbackFlow + Send + Sync + 'static,
394 {
395 Self {
396 kind: CallbackEntryKind::Progress(Box::new(callback)),
397 last_error: Mutex::new(None),
398 }
399 }
400
401 fn new_string<F>(callback: F) -> Self
402 where
403 F: Fn(LeanStringEvent) -> LeanCallbackFlow + Send + Sync + 'static,
404 {
405 Self {
406 kind: CallbackEntryKind::String(Box::new(callback)),
407 last_error: Mutex::new(None),
408 }
409 }
410
411 fn report_progress(&self, event: LeanProgressTick) -> LeanCallbackStatus {
412 let CallbackEntryKind::Progress(callback) = &self.kind else {
413 return LeanCallbackStatus::WrongPayload;
414 };
415 let result = catch_callback_panic(|| Ok(callback(event)));
416 self.flow_or_panic(result)
417 }
418
419 fn report_string(&self, event: LeanStringEvent) -> LeanCallbackStatus {
420 let CallbackEntryKind::String(callback) = &self.kind else {
421 return LeanCallbackStatus::WrongPayload;
422 };
423 let result = catch_callback_panic(|| Ok(callback(event)));
424 self.flow_or_panic(result)
425 }
426
427 fn flow_or_panic(&self, result: LeanResult<LeanCallbackFlow>) -> LeanCallbackStatus {
428 match result {
429 Ok(LeanCallbackFlow::Continue) => LeanCallbackStatus::Ok,
430 Ok(LeanCallbackFlow::Stop) => LeanCallbackStatus::Stopped,
431 Err(err) => {
432 if let Ok(mut last_error) = self.last_error.lock() {
433 *last_error = Some(err);
434 }
435 LeanCallbackStatus::Panic
436 }
437 }
438 }
439
440 fn last_error(&self) -> Option<LeanError> {
441 self.last_error.lock().ok().and_then(|guard| guard.clone())
442 }
443}
444
445fn register_entry<P: LeanCallbackPayload>(entry: CallbackEntry) -> LeanResult<LeanCallbackHandle<P>> {
446 let entry = Arc::new(entry);
447 let registry = registry();
448 let mut guard = registry
449 .lock()
450 .map_err(|_| LeanError::internal("callback registry mutex was poisoned during registration"))?;
451 let id = allocate_id(&guard)?;
452 let previous = guard.insert(id.get(), Arc::clone(&entry));
453 debug_assert!(previous.is_none(), "fresh callback id collided with an existing entry");
454 drop(guard);
455 Ok(LeanCallbackHandle {
456 id,
457 entry,
458 _payload: PhantomData,
459 })
460}
461
462fn registry() -> &'static Mutex<HashMap<usize, Arc<CallbackEntry>>> {
463 REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
464}
465
466fn allocate_id(guard: &HashMap<usize, Arc<CallbackEntry>>) -> LeanResult<NonZeroUsize> {
467 for _ in 0..1024 {
468 let raw = NEXT_ID.fetch_add(1, Ordering::Relaxed);
469 let Some(id) = NonZeroUsize::new(raw) else {
470 continue;
471 };
472 if !guard.contains_key(&id.get()) {
473 return Ok(id);
474 }
475 }
476 Err(LeanError::internal(
477 "callback registry could not allocate a fresh nonzero handle id",
478 ))
479}
480
481extern "C" fn progress_trampoline(
482 handle: usize,
483 payload_tag: u8,
484 arg0: u64,
485 arg1: u64,
486 _payload: *mut lean_object,
487) -> u8 {
488 if payload_tag != PAYLOAD_PROGRESS_TICK {
489 return LeanCallbackStatus::WrongPayload.as_abi();
490 }
491 let entry = registry().lock().ok().and_then(|guard| guard.get(&handle).cloned());
492 let Some(entry) = entry else {
493 return LeanCallbackStatus::StaleHandle.as_abi();
494 };
495 entry
496 .report_progress(LeanProgressTick {
497 current: arg0,
498 total: arg1,
499 })
500 .as_abi()
501}
502
503extern "C" fn string_trampoline(
504 handle: usize,
505 payload_tag: u8,
506 _arg0: u64,
507 _arg1: u64,
508 payload: *mut lean_object,
509) -> u8 {
510 if payload_tag != PAYLOAD_STRING {
511 return LeanCallbackStatus::WrongPayload.as_abi();
512 }
513 let entry = registry().lock().ok().and_then(|guard| guard.get(&handle).cloned());
514 let Some(entry) = entry else {
515 return LeanCallbackStatus::StaleHandle.as_abi();
516 };
517 let Some(value) = decode_string_payload(payload) else {
518 return LeanCallbackStatus::WrongPayload.as_abi();
519 };
520 entry.report_string(LeanStringEvent { value }).as_abi()
521}
522
523fn decode_string_payload(payload: *mut lean_object) -> Option<String> {
524 if payload.is_null() {
525 return None;
526 }
527 if unsafe { lean_is_scalar(payload) } {
530 return None;
531 }
532 if !unsafe { lean_is_string(payload) } {
536 return None;
537 }
538 let bytes = unsafe {
542 let size_with_nul = lean_string_size(payload);
543 let len = size_with_nul.saturating_sub(1);
544 let data = lean_string_cstr(payload).cast::<u8>();
545 slice::from_raw_parts(data, len)
546 };
547 String::from_utf8(bytes.to_vec()).ok()
548}
549
550mod private {
551 use super::{LeanProgressTick, LeanStringEvent, progress_trampoline, string_trampoline};
552
553 pub trait Sealed {
554 fn trampoline() -> usize;
555 }
556
557 impl Sealed for LeanProgressTick {
558 fn trampoline() -> usize {
559 progress_trampoline as *const () as usize
560 }
561 }
562
563 impl Sealed for LeanStringEvent {
564 fn trampoline() -> usize {
565 string_trampoline as *const () as usize
566 }
567 }
568}
569
570impl LeanCallbackPayload for LeanProgressTick {}
571impl LeanCallbackPayload for LeanStringEvent {}
572
573#[cfg(test)]
574mod tests {
575 use super::{LeanCallbackFlow, LeanCallbackHandle, LeanCallbackStatus, LeanProgressTick, LeanStringEvent};
576
577 #[test]
578 fn callback_handle_is_send_sync() {
579 fn assert_send_sync<T: Send + Sync>() {}
580 assert_send_sync::<LeanCallbackHandle<LeanProgressTick>>();
581 assert_send_sync::<LeanCallbackHandle<LeanStringEvent>>();
582 }
583
584 #[test]
585 fn status_bytes_round_trip() {
586 assert_eq!(LeanCallbackStatus::from_abi(0), Some(LeanCallbackStatus::Ok));
587 assert_eq!(LeanCallbackStatus::from_abi(1), Some(LeanCallbackStatus::StaleHandle),);
588 assert_eq!(LeanCallbackStatus::from_abi(2), Some(LeanCallbackStatus::Panic));
589 assert_eq!(LeanCallbackStatus::from_abi(3), Some(LeanCallbackStatus::WrongPayload));
590 assert_eq!(LeanCallbackStatus::from_abi(4), Some(LeanCallbackStatus::Stopped));
591 assert_eq!(LeanCallbackStatus::from_abi(5), None);
592 }
593
594 #[test]
595 fn flow_is_explicit() {
596 assert_ne!(LeanCallbackFlow::Continue, LeanCallbackFlow::Stop);
597 }
598}