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}