lean_rs_host/host/capabilities.rs
1//! `LeanCapabilities`—loaded manifest-checked host shims and optional user
2//! dylibs.
3//!
4//! [`LeanCapabilities`] owns the [`lean_rs::module::LeanLibrary`] handles and
5//! anchors the checked shim capability that [`crate::host::LeanSession`]
6//! resolves typed bindings from:
7//!
8//! - The **user's capability dylib**, when present, is the artefact the
9//! consumer built with `lake build` and named in
10//! [`crate::host::LeanHost::load_capabilities`]. The host stack initializes
11//! it so imported modules can depend on it; arbitrary user export dispatch
12//! stays in the lower-level `lean-rs` crate.
13//! - The **shim dylib** is `liblean__rs__host__shims_LeanRsHostShims.dylib`,
14//! built from the `lean-rs-host` crate's bundled shim sources. It contains
15//! the manifest-declared mandatory and optional `lean_rs_host_*` `@[export]` symbols that
16//! every typed `LeanSession` method dispatches through. Lake does *not*
17//! transitively bundle the shim's `@[export]` symbols into the user's dylib
18//! because `LeanLib.sharedFacet` emits a per-package shared library, not a
19//! transitive merge, so the host stack
20//! loads the host shim manifest, its generic interop dependency, and the
21//! user dylib explicitly. All dylibs share one Lean runtime; each per-module
22//! `initialize_<Module>` short-circuits idempotently on its own flag.
23//!
24//! A missing or mismatched mandatory shim symbol fails session construction
25//! through checked binding resolution; a missing optional shim symbol
26//! degrades to a synthesised
27//! [`crate::host::meta::LeanMetaResponse::Unsupported`] at the
28//! [`crate::LeanSession::run_meta`] call site. Bindings are resolved once
29//! per session and then cached as typed call handles—no per-query `dlsym`.
30//!
31//! Construction goes through either [`crate::host::LeanHost::load_capabilities`]
32//! or [`crate::host::LeanHost::load_shims_only`]; [`LeanCapabilities::session`]
33//! then imports a module list and returns the long-lived
34//! [`crate::host::LeanSession`] handle.
35
36use core::fmt;
37
38use lean_rs::error::LeanResult;
39use lean_rs::module::{LeanBuiltCapability, LeanCapability, LeanLibrary};
40
41use crate::host::bracketed::{LeanBracketedImportRequest, LeanBracketedImportResult};
42use crate::host::cancellation::LeanCancellationToken;
43use crate::host::host::LeanHost;
44use crate::host::progress::LeanProgressSink;
45use crate::host::session::{LeanImportProfileMode, LeanImportProfilerOptions, LeanSession, LeanSessionImportProfile};
46use crate::host::shim_bindings::host_shim_export_signatures;
47
48const HOST_CARGO_TEST_IMPORT_OVERRIDE: &str = "LEAN_RS_ALLOW_CARGO_TEST_HOST_IMPORTS";
49
50/// Loaded generic interop, host shim, and optional user dylibs with a checked
51/// host-shim capability ready for session binding resolution.
52///
53/// Owns the [`LeanLibrary`] handles so callers do not have to track
54/// the dylibs' lifetimes separately. Borrows from the parent
55/// [`LeanHost`] for the runtime + project context. Neither [`Send`] nor
56/// [`Sync`]: inherited from the contained `LeanLibrary` handles.
57pub struct LeanCapabilities<'lean, 'h> {
58 host: &'h LeanHost<'lean>,
59 /// User's capability dylib—the one named in `load_capabilities`, absent
60 /// for `load_shims_only`. Kept alive so initialized user modules remain
61 /// loaded while sessions import and query their environments.
62 _user_library: Option<LeanLibrary<'lean>>,
63 /// Manifest-backed bundled host shim capability. Session construction
64 /// resolves typed bindings from this checked surface.
65 shim_capability: LeanCapability<'lean>,
66}
67
68impl fmt::Debug for LeanCapabilities<'_, '_> {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 f.debug_struct("LeanCapabilities").finish_non_exhaustive()
71 }
72}
73
74impl<'lean, 'h> LeanCapabilities<'lean, 'h> {
75 /// Build a [`LeanCapabilities`] from an opened user-capability
76 /// library, opening the generic interop and host shim dylibs alongside it.
77 ///
78 /// Initializes the root module of the user's dylib (idempotent
79 /// through Lean's `_G_initialized` short-circuit), opens and
80 /// initializes the generic interop and host shim dylibs built from the
81 /// crate-owned shim sources, and resolves the
82 /// manifest-backed shim capability. Session construction resolves 28
83 /// mandatory and 9 optional checked typed bindings from that capability.
84 ///
85 /// # Errors
86 ///
87 /// Returns [`lean_rs::LeanError::Host`] with stage
88 /// [`lean_rs::HostStage::Load`] /
89 /// [`lean_rs::LeanDiagnosticCode::ModuleInit`] if the bundled shim dylibs
90 /// cannot be built or located. Returns [`lean_rs::HostStage::Link`] if the
91 /// initializer or any of the 28 **mandatory** symbols is missing from the
92 /// shim dylib. Missing optional meta-service symbols never fail capability
93 /// load.
94 pub(crate) fn new(
95 host: &'h LeanHost<'lean>,
96 user_library: LeanLibrary<'lean>,
97 package: &str,
98 lib_name: &str,
99 ) -> LeanResult<Self> {
100 let shim_capability = load_shim_capability(host)?;
101
102 // Now the user dylib. It does not need to depend on the host shims;
103 // ad-hoc user exports still resolve from this library.
104 let _user_module = user_library.initialize_module(package, lib_name)?;
105
106 Ok(Self {
107 host,
108 _user_library: Some(user_library),
109 shim_capability,
110 })
111 }
112
113 /// Build a [`LeanCapabilities`] backed only by the bundled interop and
114 /// host shim dylibs.
115 ///
116 /// Sessions opened from this value can use every shim-backed session
117 /// operation without loading a user capability dylib.
118 pub(crate) fn new_shims_only(host: &'h LeanHost<'lean>) -> LeanResult<Self> {
119 let shim_capability = load_shim_capability(host)?;
120 Ok(Self {
121 host,
122 _user_library: None,
123 shim_capability,
124 })
125 }
126
127 /// Import the named modules into a fresh Lean environment and
128 /// return a session over the result.
129 ///
130 /// Imports happen exactly once per `session()` call. The returned
131 /// [`LeanSession`] owns the imported environment and reuses this
132 /// capability's checked shim bindings for every query.
133 ///
134 /// # Errors
135 ///
136 /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
137 /// already cancelled before import dispatch.
138 ///
139 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side
140 /// import raises (missing `.olean`, malformed module name, …),
141 /// with the bounded message Lean surfaced.
142 pub fn session<'c>(
143 &'c self,
144 imports: &[&str],
145 cancellation: Option<&LeanCancellationToken>,
146 progress: Option<&dyn LeanProgressSink>,
147 ) -> LeanResult<LeanSession<'lean, 'c>> {
148 reject_same_process_cargo_test_session_import()?;
149 LeanSession::import(self, imports, cancellation, progress)
150 }
151
152 /// Import using an explicit full-session import profile.
153 ///
154 /// Use this when a caller intentionally needs a broader import shape than
155 /// the default private profile. No profile falls back silently to
156 /// another one: import and service failures are reported for the requested
157 /// profile.
158 ///
159 /// # Errors
160 ///
161 /// Returns a host resource error when same-process cargo-test imports are
162 /// blocked, a cancellation error when `cancellation` is already cancelled,
163 /// or the Lean/import failure surfaced by the requested profile.
164 pub fn session_with_profile<'c>(
165 &'c self,
166 imports: &[&str],
167 profile: LeanSessionImportProfile,
168 cancellation: Option<&LeanCancellationToken>,
169 progress: Option<&dyn LeanProgressSink>,
170 ) -> LeanResult<LeanSession<'lean, 'c>> {
171 reject_same_process_cargo_test_session_import()?;
172 LeanSession::import_with_profile(self, imports, profile, cancellation, progress)
173 }
174
175 /// Import using one of the closed diagnostic import modes.
176 ///
177 /// This exists for profiling import breadth only. Normal host sessions use
178 /// [`Self::session`], whose default is [`LeanSessionImportProfile::Private`].
179 ///
180 /// # Errors
181 ///
182 /// Returns a host resource error when same-process cargo-test imports are
183 /// blocked, or the Lean/import failure surfaced by the requested diagnostic
184 /// mode.
185 pub fn profiling_session<'c>(
186 &'c self,
187 imports: &[&str],
188 mode: LeanImportProfileMode,
189 profiler_options: &LeanImportProfilerOptions,
190 ) -> LeanResult<LeanSession<'lean, 'c>> {
191 reject_same_process_cargo_test_session_import()?;
192 LeanSession::import_profiled(self, imports, mode, profiler_options)
193 }
194
195 /// Run a one-shot no-extension import query inside Lean's compacted-region
196 /// bracket.
197 ///
198 /// The Lean shim uses `withImportModules`, which imports with
199 /// `loadExts := false`, serializes only the requested declaration metadata
200 /// plus import stats, frees the imported compacted regions, and then
201 /// returns Rust-owned data. It is deliberately not a replacement for
202 /// [`Self::session`]: parser, elaboration, proof-state, pretty-printing,
203 /// and capability workflows require full sessions with loaded extensions.
204 /// Extending this path to return Lean-owned objects would be unsound unless
205 /// their lifetime across `Environment.freeRegions` can be proven locally.
206 ///
207 /// # Errors
208 ///
209 /// Returns the Lean/import or ABI-conversion failure encountered while
210 /// running the closed bracketed query.
211 pub fn bracketed_import_query(
212 &self,
213 imports: &[&str],
214 request: LeanBracketedImportRequest,
215 progress: Option<&dyn LeanProgressSink>,
216 ) -> LeanResult<LeanBracketedImportResult> {
217 LeanBracketedImportResult::query(self, imports, request, progress)
218 }
219
220 /// The capability's parent host (for runtime + project access by
221 /// the session dispatch).
222 pub(crate) fn host(&self) -> &'h LeanHost<'lean> {
223 self.host
224 }
225
226 /// Manifest-backed bundled host shim capability.
227 pub(crate) fn shim_capability(&self) -> &LeanCapability<'lean> {
228 &self.shim_capability
229 }
230}
231
232fn load_shim_capability<'lean>(host: &LeanHost<'lean>) -> LeanResult<LeanCapability<'lean>> {
233 let built = crate::host::lake::LakeProject::shim_capability(host_shim_export_signatures())?;
234 LeanCapability::from_build_manifest(
235 host.runtime(),
236 LeanBuiltCapability::manifest_path(built.manifest_path()),
237 )
238}
239
240fn reject_same_process_cargo_test_session_import() -> LeanResult<()> {
241 if !same_process_cargo_test_import_guard_active() {
242 return Ok(());
243 }
244
245 Err(lean_rs::__host_internals::host_resource_exhausted(format!(
246 "lean-rs-host full-session imports are disabled under same-process cargo test; \
247 run `cargo nextest run -p lean-rs-host ...` for process-per-test isolation, \
248 or set {HOST_CARGO_TEST_IMPORT_OVERRIDE}=1 for an explicitly budgeted single-test debug run"
249 )))
250}
251
252fn same_process_cargo_test_import_guard_active() -> bool {
253 same_process_cargo_test_import_guard_active_from_facts(
254 std::env::var(HOST_CARGO_TEST_IMPORT_OVERRIDE).ok().as_deref(),
255 std::env::var_os("NEXTEST").is_some() || std::env::var_os("NEXTEST_RUN_ID").is_some(),
256 is_probably_cargo_test_binary(),
257 )
258}
259
260fn same_process_cargo_test_import_guard_active_from_facts(
261 override_value: Option<&str>,
262 nextest_present: bool,
263 is_cargo_test_binary: bool,
264) -> bool {
265 if env_value_truthy(override_value) || nextest_present {
266 return false;
267 }
268 is_cargo_test_binary
269}
270
271fn env_value_truthy(value: Option<&str>) -> bool {
272 value.is_some_and(|value| matches!(value, "1" | "true" | "TRUE" | "yes" | "YES" | "on" | "ON"))
273}
274
275fn is_probably_cargo_test_binary() -> bool {
276 let Ok(exe) = std::env::current_exe() else {
277 return false;
278 };
279 let Some(parent) = exe.parent() else {
280 return false;
281 };
282
283 // Cargo/libtest integration and unit test harnesses live under
284 // `target/{profile}/deps`. Normal examples, profiling binaries, and worker
285 // child processes do not.
286 parent
287 .file_name()
288 .is_some_and(|name| name == std::ffi::OsStr::new("deps"))
289}
290
291#[cfg(test)]
292mod tests {
293 use super::same_process_cargo_test_import_guard_active_from_facts;
294
295 #[test]
296 fn cargo_test_import_guard_blocks_same_process_libtest_harnesses() {
297 assert!(same_process_cargo_test_import_guard_active_from_facts(
298 None, false, true,
299 ));
300 }
301
302 #[test]
303 fn cargo_test_import_guard_allows_nextest_override_and_non_test_binaries() {
304 assert!(!same_process_cargo_test_import_guard_active_from_facts(
305 None, true, true,
306 ));
307 assert!(!same_process_cargo_test_import_guard_active_from_facts(
308 Some("1"),
309 false,
310 true,
311 ));
312 assert!(!same_process_cargo_test_import_guard_active_from_facts(
313 None, false, false,
314 ));
315 }
316}