Skip to main content

lean_rs/runtime/
thread.rs

1//! RAII attach guard for OS threads that did not originate inside Lean.
2//!
3//! Code that calls into Lean from a thread the Lean runtime did not start
4//! itself must attach that thread before any Lean work and detach it
5//! before the thread exits. [`LeanThreadGuard`] is the RAII type that
6//! does both: [`LeanThreadGuard::attach`] performs the attach,
7//! [`LeanThreadGuard::drop`] performs the detach.
8//!
9//! Every thread that successfully called [`super::LeanRuntime::init`] is
10//! implicitly marked as permitted to invoke Lean from that thread. The
11//! [`debug_assert_attached`] check at the host-call funnel is therefore
12//! a defensive instrument: in correctly-typed code it always passes
13//! (every host call requires a `&'lean LeanRuntime`, which is `!Send`,
14//! so the holding thread necessarily went through `init` on this
15//! thread). The assertion catches unsafe-code misuse and accidental
16//! `Send` impls.
17
18// SAFETY DOC: each `unsafe { ... }` block carries its own `// SAFETY:`
19// comment. The blanket allow keeps the unsafe surface in the smallest
20// `pub(crate)` scope that compiles, per the safety model in
21// `docs/architecture/01-safety-model.md`.
22#![allow(unsafe_code)]
23
24use std::cell::Cell;
25use std::marker::PhantomData;
26
27use super::LeanRuntime;
28
29thread_local! {
30    /// Per-thread attach depth. `0` means the current OS thread has
31    /// never called [`super::LeanRuntime::init`] on this thread and does
32    /// not hold a [`LeanThreadGuard`]; invoking Lean from such a thread
33    /// is a soundness bug.
34    ///
35    /// Every successful `init()` call lifts the floor to `1` (via
36    /// [`mark_calling_thread_permitted`]). Each [`LeanThreadGuard`]
37    /// further increments on `attach` and decrements on `Drop`.
38    /// Nesting is legal and pairs depth-for-depth.
39    static ATTACH_DEPTH: Cell<u32> = const { Cell::new(0) };
40}
41
42/// Mark the calling thread as permitted to invoke Lean from this thread.
43///
44/// Called from [`super::LeanRuntime::init`] on every successful return,
45/// not only the first. Subsequent calls on the same thread are no-ops:
46/// the depth never drops below the permanent floor of `1` that `init`
47/// installs. The init thread is, by this rule, also covered — it called
48/// `init` on itself like every other client.
49pub(crate) fn mark_calling_thread_permitted() {
50    ATTACH_DEPTH.with(|d| {
51        if d.get() == 0 {
52            d.set(1);
53        }
54    });
55}
56
57/// Debug-mode assertion that the current thread is attached to Lean.
58///
59/// Inserted at every host call funnel (notably
60/// [`crate::module::LeanExported::call`]) so that a worker thread that
61/// forgets to construct a [`LeanThreadGuard`] panics with a clear Rust
62/// message instead of tripping a Lean-side assertion. Compiles to a
63/// no-op in release builds.
64///
65/// The `stage` argument is the textual label included in the panic
66/// message; pass a `&'static str` identifying the call site.
67pub(crate) fn debug_assert_attached(stage: &'static str) {
68    debug_assert!(
69        ATTACH_DEPTH.with(|d| d.get()) >= 1,
70        "lean-rs: {stage} invoked on a thread that is not attached to the Lean runtime; \
71         construct a `lean_rs::LeanThreadGuard` on this thread first \
72         (see docs/architecture/04-concurrency.md)",
73    );
74}
75
76/// RAII handle that attaches the current OS thread to the Lean runtime.
77///
78/// When constructed via [`LeanThreadGuard::attach`], the guard registers
79/// the calling thread with the Lean runtime so that thread may invoke
80/// compiled Lean code. When the guard is dropped (or its scope ends), the
81/// thread is detached again. Threads spawned by Lean itself are already
82/// attached and do **not** need a guard; the thread that called
83/// [`crate::LeanRuntime::init`] is likewise treated as attached for its
84/// entire lifetime.
85///
86/// The guard is neither [`Send`] nor [`Sync`]: it represents a per-thread
87/// resource, and shipping it to a different OS thread to be dropped would
88/// detach the wrong thread. The `'lean` lifetime parameter ties the guard
89/// to a [`LeanRuntime`] borrow, so a guard cannot outlive the runtime it
90/// is bound to.
91///
92/// Nested attaches are legal: a worker thread already inside an outer
93/// `LeanThreadGuard` may construct another (e.g. inside a callback) and
94/// the per-thread attach depth tracks the pairs so the host-call
95/// attach assertion does not fire prematurely.
96///
97/// # Example
98///
99/// ```ignore
100/// // Inside a worker thread you spawned yourself:
101/// let runtime = lean_rs::LeanRuntime::init()?;
102/// let _guard = lean_rs::LeanThreadGuard::attach(runtime);
103/// // ... call into Lean from this thread ...
104/// // `_guard` is dropped at scope exit, detaching the thread cleanly.
105/// ```
106pub struct LeanThreadGuard<'lean> {
107    _runtime: PhantomData<&'lean LeanRuntime>,
108    _no_send_no_sync: PhantomData<*mut ()>,
109}
110
111impl<'lean> LeanThreadGuard<'lean> {
112    /// Attach the current OS thread to the Lean runtime.
113    ///
114    /// Construction requires a borrow of the process [`LeanRuntime`]
115    /// handle; that borrow is the type-level proof that the runtime has
116    /// been initialized on this process. The returned guard must be
117    /// dropped on the same thread that attached it.
118    #[must_use = "the guard detaches the thread on Drop; bind it to a name"]
119    pub fn attach(_runtime: &'lean LeanRuntime) -> Self {
120        // SAFETY: We hold a `&LeanRuntime` borrow, so the Lean runtime is
121        // initialized on this process. Attaching the current OS thread has
122        // no other precondition; the guard's `Drop` is the only path that
123        // detaches, and the guard is `!Send` so it cannot be detached on
124        // a different thread.
125        unsafe {
126            lean_rs_sys::init::lean_initialize_thread();
127        }
128        ATTACH_DEPTH.with(|d| d.set(d.get().saturating_add(1)));
129        Self {
130            _runtime: PhantomData,
131            _no_send_no_sync: PhantomData,
132        }
133    }
134}
135
136impl Drop for LeanThreadGuard<'_> {
137    fn drop(&mut self) {
138        // SAFETY: Paired with the attach call in `LeanThreadGuard::attach`.
139        // Construction of `LeanThreadGuard` is the only path that produces
140        // a value of this type, and the type is `!Send`, so this `Drop`
141        // necessarily runs on the same OS thread that performed the
142        // attach.
143        unsafe {
144            lean_rs_sys::init::lean_finalize_thread();
145        }
146        ATTACH_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
147    }
148}