Skip to main content

lean_rs/runtime/
init.rs

1//! Process-once Lean runtime initialization.
2//!
3//! The exact `lean_*` symbols and the order they are invoked are
4//! intentionally not part of the public surface; see
5//! `docs/architecture/01-safety-model.md`.
6
7// SAFETY DOC: every `unsafe { ... }` block in this file carries its own
8// `// SAFETY:` comment naming the invariant. The blanket allow exists
9// because this is a `pub(crate)` module that bridges the safe `lean-rs`
10// boundary to raw `lean_rs_sys::init::*` entry points, per the safety
11// model's "smallest scope that compiles" rule. Items below stay
12// `pub(crate)` except for the `LeanRuntime` type and its `init`
13// method, which are the only outward-facing pieces of this layer.
14#![allow(unsafe_code)]
15
16use std::marker::PhantomData;
17use std::panic::{self, AssertUnwindSafe};
18use std::ptr::NonNull;
19use std::sync::OnceLock;
20
21#[cfg(doc)]
22use crate::error::HostStage;
23use crate::error::{LeanError, LeanResult};
24
25/// Handle for the process-wide Lean runtime.
26///
27/// `LeanRuntime` is a zero-sized type with no public constructor. The only
28/// way to obtain one is to call [`LeanRuntime::init`], which returns a
29/// `'static` borrow once the Lean runtime is up. Every later handle
30/// (`Obj<'lean>`, `LeanExpr<'lean>`, `LeanSession<'lean, '_>`, …) carries
31/// a `'lean` lifetime tied to this borrow, so a value derived from Lean
32/// cannot outlive the runtime that produced it. This is the
33/// type-system anchor for the `'lean` cascade described in
34/// `docs/architecture/03-host-stack.md`.
35///
36/// Neither [`Send`] nor [`Sync`]. The Lean runtime is per-thread, and
37/// shipping a Lean-derived handle to another OS thread is a soundness
38/// hazard rather than an ergonomic choice; the `!Sync` claim here forces
39/// `&'lean LeanRuntime` to be `!Send`, and every downstream handle that
40/// holds `PhantomData<&'lean LeanRuntime>` inherits the same restriction.
41pub struct LeanRuntime {
42    _no_send_no_sync: PhantomData<*mut ()>,
43}
44
45impl LeanRuntime {
46    /// Initialize the Lean runtime if it has not already been initialized,
47    /// and return a `'static` borrow that anchors the `'lean` lifetime
48    /// cascade.
49    ///
50    /// Idempotent and safe to call from any thread: the underlying
51    /// initialization runs exactly once for the lifetime of the process.
52    /// Subsequent calls — including calls from other threads — return the
53    /// same borrow, or replay the cached failure if the first attempt
54    /// failed.
55    ///
56    /// # Worker threads
57    ///
58    /// `init` starts a process-wide Lean task manager. The worker thread
59    /// count is Lean's compiled-in default — typically one worker per
60    /// hardware core — unless the `LEAN_RS_NUM_THREADS` environment
61    /// variable is set to a positive integer before the first call to
62    /// `init`. The first call captures the value; later changes to the
63    /// variable have no effect. Set `LEAN_RS_NUM_THREADS` when several
64    /// Lean-using processes run side by side (CI test matrices, batch
65    /// jobs, multi-tenant workers) to avoid oversubscribing cores. The
66    /// pool is process-lifetime; there is no `set_num_threads`-style
67    /// reconfiguration once `init` has run.
68    ///
69    /// # Errors
70    ///
71    /// Returns a [`LeanError::Host`] with stage [`HostStage::RuntimeInit`]
72    /// if initialization failed. Today the only reachable failure is a
73    /// caught panic from the Lean `lean_initialize_*` entry points; the
74    /// panic payload is rendered into a bounded message so it cannot
75    /// unwind into Lean or C frames.
76    pub fn init() -> LeanResult<&'static Self> {
77        let _span = tracing::info_span!(target: "lean_rs", "lean_rs.runtime.init").entered();
78        match INIT.get_or_init(do_initialize_once) {
79            Ok(()) => {
80                // The build script already accepts only `lean.h` digests in
81                // the [`SUPPORTED_TOOLCHAINS`](lean_rs_sys::SUPPORTED_TOOLCHAINS)
82                // window, so this probe is a belt over the buckle: it catches
83                // a hand-edited `consts.rs` or a stale build artifact, and it
84                // emits the resolved version into the trace span on every
85                // initialization so operators can see which toolchain is
86                // actually live. Lean's C ABI exposes no runtime version
87                // query (`lean::get_short_version_string` is a C++ symbol
88                // not part of the documented surface), so a live-library
89                // substitution after build cannot be detected here.
90                if lean_rs_sys::supported_for(lean_rs_sys::LEAN_VERSION).is_none() {
91                    tracing::error!(
92                        target: "lean_rs",
93                        code = crate::error::LeanDiagnosticCode::RuntimeInit.as_str(),
94                        version = lean_rs_sys::LEAN_VERSION,
95                        "active Lean toolchain not in the supported window",
96                    );
97                    return Err(LeanError::runtime_init_unsupported_toolchain(lean_rs_sys::LEAN_VERSION));
98                }
99                tracing::debug!(
100                    target: "lean_rs",
101                    resolved_version = lean_rs_sys::LEAN_RESOLVED_VERSION,
102                    discovered_version = lean_rs_sys::LEAN_VERSION,
103                    "Lean runtime initialized",
104                );
105                // Every successful `init()` mints the caller's permission
106                // to invoke Lean from the calling thread. The first call
107                // also runs the C-level `lean_initialize*` sequence inside
108                // `do_initialize_once`; subsequent calls (including those
109                // from other threads) only mark the local TLS depth. The
110                // call is idempotent and `!Send`-coherent: every safe
111                // handle requires a `&'lean LeanRuntime` to construct, so
112                // any thread that reaches a host call has provably been
113                // through `init()` on that thread.
114                super::thread::mark_calling_thread_permitted();
115                Ok(static_ref())
116            }
117            Err(err) => {
118                tracing::error!(
119                    target: "lean_rs",
120                    code = crate::error::LeanDiagnosticCode::RuntimeInit.as_str(),
121                    "Lean runtime initialization failed",
122                );
123                Err(err.clone())
124            }
125        }
126    }
127}
128
129impl std::fmt::Debug for LeanRuntime {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        f.debug_struct("LeanRuntime").finish()
132    }
133}
134
135/// Process-wide initialization cell.
136///
137/// Stores the success-or-failure outcome of the first initialization
138/// attempt so every later [`LeanRuntime::init`] call replays the same
139/// result. Storing the outcome (rather than only a "did it run?" flag) is
140/// what lets `init()` hand back the same typed error on retry without
141/// re-entering the C frames a second time.
142static INIT: OnceLock<Result<(), LeanError>> = OnceLock::new();
143
144/// Run the C-level initialization sequence under a panic boundary.
145///
146/// `OnceLock::get_or_init` guarantees this runs at most once per process,
147/// which discharges the "call exactly once" obligation of the underlying
148/// Lean entry points.
149fn do_initialize_once() -> Result<(), LeanError> {
150    let outcome = panic::catch_unwind(AssertUnwindSafe(|| {
151        let workers = read_num_threads_env();
152        // SAFETY: All three calls are valid to invoke once per process
153        // before any other Lean code runs; `OnceLock::get_or_init`
154        // enforces the "once" half of the contract. The order
155        // (`runtime_module` before the full `initialize`; task manager
156        // last) follows the documented Lean embedding sequence captured
157        // in `crates/lean-rs-sys/src/init.rs`. None of the calls take
158        // inputs from Rust state (the `workers` count is a plain `u32`
159        // read from the environment above), so there is no aliasing or
160        // lifetime hazard. The task manager is required for any code
161        // path that spawns Lean tasks — including
162        // `Lean.Elab.Frontend.process` (driven by `kernel_check`),
163        // which would otherwise abort with a
164        // "g_task_manager" assertion on the first
165        // `Language.Lean.processCommands` call. Lean tears the manager
166        // down at process exit; we hold no Drop handle here because
167        // the runtime itself is intentionally process-lifetime.
168        unsafe {
169            lean_rs_sys::init::lean_initialize_runtime_module();
170            lean_rs_sys::init::lean_initialize();
171            match workers {
172                Some(n) => lean_rs_sys::init::lean_init_task_manager_using(n),
173                None => lean_rs_sys::init::lean_init_task_manager(),
174            }
175        }
176    }));
177    match outcome {
178        Ok(()) => Ok(()),
179        Err(payload) => Err(LeanError::runtime_init_panic(payload.as_ref())),
180    }
181}
182
183/// Parse the `LEAN_RS_NUM_THREADS` environment variable for the worker
184/// count to hand to `lean_init_task_manager_using`.
185///
186/// Returns `Some(n)` for any positive integer; returns `None` (Lean's
187/// compiled-in default) if the variable is unset or holds a value that
188/// is not a positive integer. Invalid values emit a single `warn!`
189/// against the `lean_rs` target so the operator notices the typo without
190/// breaking the run.
191fn read_num_threads_env() -> Option<u32> {
192    let raw = std::env::var("LEAN_RS_NUM_THREADS").ok()?;
193    match raw.trim().parse::<u32>() {
194        Ok(n) if n >= 1 => Some(n),
195        _ => {
196            tracing::warn!(
197                target: "lean_rs",
198                value = %raw,
199                "LEAN_RS_NUM_THREADS must be a positive integer; falling back to the Lean default",
200            );
201            None
202        }
203    }
204}
205
206/// Hand out a `'static` borrow of the zero-sized [`LeanRuntime`] anchor.
207fn static_ref() -> &'static LeanRuntime {
208    // SAFETY: `LeanRuntime` is a zero-sized type, so any aligned non-null
209    // pointer is a valid `*const LeanRuntime`. `NonNull::dangling()`
210    // returns exactly such a pointer. The resulting borrow carries no
211    // bytes; the Lean runtime, once initialized, is never finalized for
212    // the remainder of the process, so a `'static` lifetime is honest.
213    // The borrow's `!Sync` (and therefore `&LeanRuntime: !Send`) property
214    // is preserved by the type itself, not by where it is stored. This is
215    // the standard ZST-handle pattern (cf. PyO3's `Bound`).
216    unsafe { NonNull::<LeanRuntime>::dangling().as_ref() }
217}