lean_rs_host/host/capabilities.rs
1//! `LeanCapabilities` — loaded user, generic interop, and host shim
2//! dylibs with session symbol addresses pre-resolved.
3//!
4//! [`LeanCapabilities`] owns three [`lean_rs::module::LeanLibrary`]
5//! handles and caches the session symbol addresses that
6//! [`crate::host::LeanSession`] dispatches through:
7//!
8//! - The **user's capability dylib** is the artefact the consumer
9//! built with `lake build` and named in
10//! [`crate::host::LeanHost::load_capabilities`]. It contains the
11//! user's own `@[export]` symbols ([`crate::LeanSession::call_capability`]
12//! dispatches 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 [`crate::host::LeanHost::load_capabilities`];
35//! [`LeanCapabilities::session`] then imports a module list and returns
36//! the long-lived [`crate::host::LeanSession`] handle.
37
38use core::fmt;
39
40use crate::host::cancellation::LeanCancellationToken;
41use crate::host::host::LeanHost;
42use crate::host::progress::LeanProgressSink;
43use crate::host::session::{LeanSession, SessionSymbols};
44use lean_rs::error::LeanResult;
45use lean_rs::module::LeanLibrary;
46
47/// Loaded capability, generic interop, and host shim dylibs with session
48/// symbol addresses pre-resolved.
49///
50/// Owns the [`LeanLibrary`] handles so callers do not have to track
51/// the dylibs' lifetimes separately. Borrows from the parent
52/// [`LeanHost`] for the runtime + project context. Neither [`Send`] nor
53/// [`Sync`]: inherited from the contained `LeanLibrary` handles.
54pub struct LeanCapabilities<'lean, 'h> {
55 host: &'h LeanHost<'lean>,
56 /// User's capability dylib — the one named in `load_capabilities`.
57 /// `pub(crate)` accessor below exposes it to
58 /// [`crate::LeanSession::call_capability`] for ad-hoc dispatch on
59 /// user-authored `@[export]` symbols.
60 user_library: LeanLibrary<'lean>,
61 /// Generic interop shim dylib carrying reusable callback helpers used by
62 /// host progress shims. Loaded globally before the host shim dylib so
63 /// generated `LeanRsInterop.*` initializer references resolve.
64 #[allow(dead_code, reason = "Drop releases the dylib; field is structurally required")]
65 interop_library: LeanLibrary<'lean>,
66 /// Shim dylib carrying the 28+6 `lean_rs_host_*` `@[export]`
67 /// symbols. RAII anchor only — the addresses inside `symbols`
68 /// outlive any direct read of this field.
69 #[allow(dead_code, reason = "Drop releases the dylib; field is structurally required")]
70 shim_library: LeanLibrary<'lean>,
71 symbols: SessionSymbols,
72}
73
74impl fmt::Debug for LeanCapabilities<'_, '_> {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 f.debug_struct("LeanCapabilities").finish_non_exhaustive()
77 }
78}
79
80impl<'lean, 'h> LeanCapabilities<'lean, 'h> {
81 /// Build a [`LeanCapabilities`] from an opened user-capability
82 /// library, opening the generic interop and host shim dylibs alongside it.
83 ///
84 /// Initializes the root module of the user's dylib (idempotent
85 /// through Lean's `_G_initialized` short-circuit), opens and
86 /// initializes the generic interop and host shim dylibs built from the
87 /// crate-owned shim sources, and resolves the
88 /// session-dispatch symbol addresses from the **shim** dylib: 28
89 /// mandatory baseline symbols (load failure on miss) and 6
90 /// optional symbols — five bounded `MetaM` services plus the
91 /// info-tree projection (missing entries stored as `None`). Both
92 /// [`lean_rs::module::LeanModule`] handles are
93 /// dropped at the end of this call — the cached symbol addresses
94 /// outlive any single module borrow.
95 ///
96 /// # Errors
97 ///
98 /// Returns [`lean_rs::LeanError::Host`] with stage
99 /// [`lean_rs::HostStage::Load`] /
100 /// [`lean_rs::LeanDiagnosticCode::ModuleInit`] if the bundled shim dylibs
101 /// cannot be built or located. Returns [`lean_rs::HostStage::Link`] if the
102 /// initializer or any of the 28 **mandatory** symbols is missing from the
103 /// shim dylib. Missing optional meta-service symbols never fail capability
104 /// load.
105 pub(crate) fn new(
106 host: &'h LeanHost<'lean>,
107 user_library: LeanLibrary<'lean>,
108 package: &str,
109 lib_name: &str,
110 ) -> LeanResult<Self> {
111 // Load order matters. The host shim imports LeanRsInterop.Callback,
112 // so the generic interop dylib must be global before the host shim
113 // initializer runs. Consumers no longer require host shims in their
114 // Lake package; the host stack opens and initializes both bundled shim
115 // dylibs explicitly before the user dylib initializes.
116 let interop_dylib_path = crate::host::lake::LakeProject::interop_dylib()?;
117 let interop_library = LeanLibrary::open_globally(host.runtime(), &interop_dylib_path)?;
118 let _interop_module = interop_library.initialize_module(
119 crate::host::lake::INTEROP_PACKAGE_NAME,
120 crate::host::lake::INTEROP_LIB_NAME,
121 )?;
122
123 let shim_dylib_path = crate::host::lake::LakeProject::shim_dylib()?;
124 let shim_library = LeanLibrary::open_globally(host.runtime(), &shim_dylib_path)?;
125 // Explicitly initialize the shim's root module too, so the
126 // shim's @[export] functions are live regardless of whether
127 // the user's chain reaches them transitively.
128 let _shim_module =
129 shim_library.initialize_module(crate::host::lake::SHIM_PACKAGE_NAME, crate::host::lake::SHIM_LIB_NAME)?;
130
131 // Now the user dylib. It does not need to depend on the host shims;
132 // ad-hoc user exports still resolve from this library.
133 let _user_module = user_library.initialize_module(package, lib_name)?;
134
135 // The 28 mandatory + 6 optional `lean_rs_host_*` symbols live
136 // in the shim dylib; resolve them there.
137 // `LeanSession::call_capability` (separately) routes ad-hoc
138 // user-authored `@[export]` symbols through `user_library`.
139 let symbols = SessionSymbols::resolve(&shim_library)?;
140 Ok(Self {
141 host,
142 user_library,
143 interop_library,
144 shim_library,
145 symbols,
146 })
147 }
148
149 /// Import the named modules into a fresh Lean environment and
150 /// return a session over the result.
151 ///
152 /// Imports happen exactly once per `session()` call. The returned
153 /// [`LeanSession`] owns the imported environment and reuses this
154 /// capability's cached symbol addresses for every query.
155 ///
156 /// # Errors
157 ///
158 /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
159 /// already cancelled before import dispatch.
160 ///
161 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side
162 /// import raises (missing `.olean`, malformed module name, …),
163 /// with the bounded message Lean surfaced.
164 pub fn session<'c>(
165 &'c self,
166 imports: &[&str],
167 cancellation: Option<&LeanCancellationToken>,
168 progress: Option<&dyn LeanProgressSink>,
169 ) -> LeanResult<LeanSession<'lean, 'c>> {
170 LeanSession::import(self, imports, cancellation, progress)
171 }
172
173 /// The capability's parent host (for runtime + project access by
174 /// the session dispatch).
175 pub(crate) fn host(&self) -> &'h LeanHost<'lean> {
176 self.host
177 }
178
179 /// The pre-resolved session symbol addresses.
180 pub(crate) fn symbols(&self) -> &SessionSymbols {
181 &self.symbols
182 }
183
184 /// The user's owned capability [`LeanLibrary`].
185 ///
186 /// `pub(crate)` so [`crate::LeanSession::call_capability`] can
187 /// resolve ad-hoc function symbols on the user's dylib without
188 /// holding a separate library borrow. Ad-hoc calls always go to
189 /// the user's dylib, not the shim dylib: the shim dylib hosts a
190 /// fixed contract (the 28+6 `lean_rs_host_*` symbols pre-resolved
191 /// in `symbols`); arbitrary user `@[export]` symbols live in the
192 /// user's dylib.
193 pub(crate) fn library(&self) -> &LeanLibrary<'lean> {
194 &self.user_library
195 }
196}