lean_rs_host/host/capabilities.rs
1//! `LeanCapabilities` — loaded generic interop, host shim, and optional user
2//! dylibs with session symbol addresses pre-resolved.
3//!
4//! [`LeanCapabilities`] owns the [`lean_rs::module::LeanLibrary`] handles and
5//! caches the session symbol addresses that
6//! [`crate::host::LeanSession`] dispatches through:
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`]. It contains the user's own
11//! `@[export]` symbols ([`crate::LeanSession::call_capability`] dispatches
12//! here).
13//! - The **generic interop dylib** is
14//! `liblean__rs__interop__shims_LeanRsInterop.dylib`; it carries
15//! reusable callback helpers imported by the host progress shims.
16//! - The **shim dylib** is `liblean__rs__host__shims_LeanRsHostShims.dylib`,
17//! built from the `lean-rs-host` crate's bundled shim sources. It contains
18//! the 28 mandatory + 6 optional `lean_rs_host_*` `@[export]` symbols that
19//! every typed `LeanSession` method dispatches through. Lake does *not*
20//! transitively bundle the shim's `@[export]` symbols into the user's dylib
21//! (verified at carve-out time, 2026-05-18 — `LeanLib.sharedFacet` is a
22//! per-package shared library, not a transitive merge), so the host stack
23//! loads the generic interop dylib, host shim dylib, and user dylib
24//! explicitly. All dylibs share one Lean runtime; each per-module
25//! `initialize_<Module>` short-circuits idempotently on its own flag.
26//!
27//! A missing mandatory shim symbol fails capability load; a missing
28//! meta-service symbol degrades to a synthesised
29//! [`crate::host::meta::LeanMetaResponse::Unsupported`] at the
30//! [`crate::LeanSession::run_meta`] call site. Pre-resolution at
31//! construction means each later query is one struct-field read and
32//! one FFI call — no per-query `dlsym`.
33//!
34//! Construction goes through either [`crate::host::LeanHost::load_capabilities`]
35//! or [`crate::host::LeanHost::load_shims_only`]; [`LeanCapabilities::session`]
36//! then imports a module list and returns the long-lived
37//! [`crate::host::LeanSession`] handle.
38
39use core::fmt;
40
41use crate::host::cancellation::LeanCancellationToken;
42use crate::host::host::LeanHost;
43use crate::host::progress::LeanProgressSink;
44use crate::host::session::{LeanSession, SessionSymbols};
45use lean_rs::error::LeanResult;
46use lean_rs::module::LeanLibrary;
47
48/// Loaded generic interop, host shim, and optional user dylibs with session
49/// symbol addresses pre-resolved.
50///
51/// Owns the [`LeanLibrary`] handles so callers do not have to track
52/// the dylibs' lifetimes separately. Borrows from the parent
53/// [`LeanHost`] for the runtime + project context. Neither [`Send`] nor
54/// [`Sync`]: inherited from the contained `LeanLibrary` handles.
55pub struct LeanCapabilities<'lean, 'h> {
56 host: &'h LeanHost<'lean>,
57 /// User's capability dylib — the one named in `load_capabilities`, absent
58 /// for `load_shims_only`.
59 /// `pub(crate)` accessor below exposes it to
60 /// [`crate::LeanSession::call_capability`] for ad-hoc dispatch on
61 /// user-authored `@[export]` symbols.
62 user_library: Option<LeanLibrary<'lean>>,
63 /// Generic interop shim dylib carrying reusable callback helpers used by
64 /// host progress shims. Loaded globally before the host shim dylib so
65 /// generated `LeanRsInterop.*` initializer references resolve.
66 #[allow(dead_code, reason = "Drop releases the dylib; field is structurally required")]
67 interop_library: LeanLibrary<'lean>,
68 /// Shim dylib carrying the 28+6 `lean_rs_host_*` `@[export]`
69 /// symbols. RAII anchor only — the addresses inside `symbols`
70 /// outlive any direct read of this field.
71 #[allow(dead_code, reason = "Drop releases the dylib; field is structurally required")]
72 shim_library: LeanLibrary<'lean>,
73 symbols: SessionSymbols,
74}
75
76impl fmt::Debug for LeanCapabilities<'_, '_> {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 f.debug_struct("LeanCapabilities").finish_non_exhaustive()
79 }
80}
81
82impl<'lean, 'h> LeanCapabilities<'lean, 'h> {
83 /// Build a [`LeanCapabilities`] from an opened user-capability
84 /// library, opening the generic interop and host shim dylibs alongside it.
85 ///
86 /// Initializes the root module of the user's dylib (idempotent
87 /// through Lean's `_G_initialized` short-circuit), opens and
88 /// initializes the generic interop and host shim dylibs built from the
89 /// crate-owned shim sources, and resolves the
90 /// session-dispatch symbol addresses from the **shim** dylib: 28
91 /// mandatory baseline symbols (load failure on miss) and 6
92 /// optional symbols — five bounded `MetaM` services plus the
93 /// info-tree projection (missing entries stored as `None`). Both
94 /// [`lean_rs::module::LeanModule`] handles are
95 /// dropped at the end of this call — the cached symbol addresses
96 /// outlive any single module borrow.
97 ///
98 /// # Errors
99 ///
100 /// Returns [`lean_rs::LeanError::Host`] with stage
101 /// [`lean_rs::HostStage::Load`] /
102 /// [`lean_rs::LeanDiagnosticCode::ModuleInit`] if the bundled shim dylibs
103 /// cannot be built or located. Returns [`lean_rs::HostStage::Link`] if the
104 /// initializer or any of the 28 **mandatory** symbols is missing from the
105 /// shim dylib. Missing optional meta-service symbols never fail capability
106 /// load.
107 pub(crate) fn new(
108 host: &'h LeanHost<'lean>,
109 user_library: LeanLibrary<'lean>,
110 package: &str,
111 lib_name: &str,
112 ) -> LeanResult<Self> {
113 let ShimLibraries {
114 interop_library,
115 shim_library,
116 symbols,
117 } = load_shim_libraries(host)?;
118
119 // Now the user dylib. It does not need to depend on the host shims;
120 // ad-hoc user exports still resolve from this library.
121 let _user_module = user_library.initialize_module(package, lib_name)?;
122
123 Ok(Self {
124 host,
125 user_library: Some(user_library),
126 interop_library,
127 shim_library,
128 symbols,
129 })
130 }
131
132 /// Build a [`LeanCapabilities`] backed only by the bundled interop and
133 /// host shim dylibs.
134 ///
135 /// Sessions opened from this value can use every shim-backed session
136 /// operation. [`crate::LeanSession::call_capability`] returns
137 /// [`lean_rs::LeanDiagnosticCode::Unsupported`] because no user dylib is
138 /// attached.
139 pub(crate) fn new_shims_only(host: &'h LeanHost<'lean>) -> LeanResult<Self> {
140 let ShimLibraries {
141 interop_library,
142 shim_library,
143 symbols,
144 } = load_shim_libraries(host)?;
145 Ok(Self {
146 host,
147 user_library: None,
148 interop_library,
149 shim_library,
150 symbols,
151 })
152 }
153
154 /// Import the named modules into a fresh Lean environment and
155 /// return a session over the result.
156 ///
157 /// Imports happen exactly once per `session()` call. The returned
158 /// [`LeanSession`] owns the imported environment and reuses this
159 /// capability's cached symbol addresses for every query.
160 ///
161 /// # Errors
162 ///
163 /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
164 /// already cancelled before import dispatch.
165 ///
166 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side
167 /// import raises (missing `.olean`, malformed module name, …),
168 /// with the bounded message Lean surfaced.
169 pub fn session<'c>(
170 &'c self,
171 imports: &[&str],
172 cancellation: Option<&LeanCancellationToken>,
173 progress: Option<&dyn LeanProgressSink>,
174 ) -> LeanResult<LeanSession<'lean, 'c>> {
175 LeanSession::import(self, imports, cancellation, progress)
176 }
177
178 /// The capability's parent host (for runtime + project access by
179 /// the session dispatch).
180 pub(crate) fn host(&self) -> &'h LeanHost<'lean> {
181 self.host
182 }
183
184 /// The pre-resolved session symbol addresses.
185 pub(crate) fn symbols(&self) -> &SessionSymbols {
186 &self.symbols
187 }
188
189 /// The user's owned capability [`LeanLibrary`], if this capability was
190 /// loaded with a user dylib.
191 ///
192 /// `pub(crate)` so [`crate::LeanSession::call_capability`] can
193 /// resolve ad-hoc function symbols on the user's dylib without
194 /// holding a separate library borrow. Ad-hoc calls always go to
195 /// the user's dylib, not the shim dylib: the shim dylib hosts a
196 /// fixed contract (the 28+6 `lean_rs_host_*` symbols pre-resolved
197 /// in `symbols`); arbitrary user `@[export]` symbols live in the
198 /// user's dylib.
199 pub(crate) fn user_library(&self) -> Option<&LeanLibrary<'lean>> {
200 self.user_library.as_ref()
201 }
202}
203
204struct ShimLibraries<'lean> {
205 interop_library: LeanLibrary<'lean>,
206 shim_library: LeanLibrary<'lean>,
207 symbols: SessionSymbols,
208}
209
210fn load_shim_libraries<'lean>(host: &LeanHost<'lean>) -> LeanResult<ShimLibraries<'lean>> {
211 // Load order matters. The host shim imports LeanRsInterop.Callback, so
212 // the generic interop dylib must be global before the host shim
213 // initializer runs.
214 let interop_dylib_path = crate::host::lake::LakeProject::interop_dylib()?;
215 let interop_library = LeanLibrary::open_globally(host.runtime(), &interop_dylib_path)?;
216 let _interop_module = interop_library.initialize_module(
217 crate::host::lake::INTEROP_PACKAGE_NAME,
218 crate::host::lake::INTEROP_LIB_NAME,
219 )?;
220
221 let shim_dylib_path = crate::host::lake::LakeProject::shim_dylib()?;
222 let shim_library = LeanLibrary::open_globally(host.runtime(), &shim_dylib_path)?;
223 let _shim_module =
224 shim_library.initialize_module(crate::host::lake::SHIM_PACKAGE_NAME, crate::host::lake::SHIM_LIB_NAME)?;
225
226 // The 28 mandatory + 6 optional `lean_rs_host_*` symbols live in the
227 // shim dylib; resolve them there in both capability-loading modes.
228 let symbols = SessionSymbols::resolve(&shim_library)?;
229 Ok(ShimLibraries {
230 interop_library,
231 shim_library,
232 symbols,
233 })
234}