Skip to main content

lean_rs_host/host/
session.rs

1//! `LeanSession`—a long-lived Lean session over an imported
2//! environment.
3//!
4//! A [`LeanSession`] holds an imported `Lean.Environment` value (as an
5//! opaque `Obj<'lean>`) plus a borrow of its parent
6//! [`crate::host::LeanCapabilities`]. Each typed query method
7//! ([`LeanSession::query_declaration`], …) dispatches through a
8//! manifest-checked typed host-shim binding cached on the session—one
9//! struct-field read, one FFI call, no per-query `dlsym`.
10//!
11//! ## Capability contract
12//!
13//! The bundled host shim dylib that [`crate::host::LeanCapabilities`] loads
14//! exports twenty-nine **mandatory** `@[export]` symbols and may export ten
15//! **optional** symbols (checked when session bindings are constructed)—
16//! five bounded `MetaM` services plus declaration inspection, module-query
17//! entry points, and cache control:
18//!
19//! | C symbol                                               | Mandatory? | Lean signature                                                                                                                                                       |
20//! | ------------------------------------------------------ | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
21//! | `lean_rs_host_session_import`                          | yes        | `String -> Array String -> IO Environment`                                                                                                                           |
22//! | `lean_rs_host_session_import_progress`                 | yes        | `Array String -> Array String -> USize -> USize -> IO (Except UInt8 Environment)`                                                                                    |
23//! | `lean_rs_host_name_from_string`                        | yes        | `String -> Name`                                                                                                                                                     |
24//! | `lean_rs_host_name_to_string`                          | yes        | `Name -> String`                                                                                                                                                     |
25//! | `lean_rs_host_env_query_declaration`                   | yes        | `Environment -> Name -> IO (Option Declaration)`                                                                                                                     |
26//! | `lean_rs_host_env_query_declarations_bulk`             | yes        | `Environment -> Array Name -> IO (Array (Option Declaration))`                                                                                                       |
27//! | `lean_rs_host_env_query_declarations_bulk_progress`    | yes        | `Environment -> Array Name -> USize -> USize -> IO (Except UInt8 (Array (Option Declaration)))`                                                                      |
28//! | `lean_rs_host_env_list_declarations`                   | yes        | `Environment -> IO (Array Name)`                                                                                                                                     |
29//! | `lean_rs_host_env_list_declarations_filtered`          | yes        | `Environment -> DeclarationFilter -> IO (Array Name)`                                                                                                                |
30//! | `lean_rs_host_env_list_declarations_filtered_progress` | yes        | `Environment -> DeclarationFilter -> USize -> USize -> IO (Except UInt8 (Array Name))`                                                                               |
31//! | `lean_rs_host_env_declaration_source_range`            | yes        | `Environment -> Name -> Array String -> IO (Option SourceRange)`                                                                                                     |
32//! | `lean_rs_host_env_declaration_type`                    | yes        | `Environment -> Name -> IO (Option Expr)`                                                                                                                            |
33//! | `lean_rs_host_env_declaration_type_bulk`               | yes        | `Environment -> Array String -> IO (Array (Option Expr))`                                                                                                            |
34//! | `lean_rs_host_env_declaration_type_bulk_progress`      | yes        | `Environment -> Array String -> USize -> USize -> IO (Except UInt8 (Array (Option Expr)))`                                                                           |
35//! | `lean_rs_host_env_declaration_kind`                    | yes        | `Environment -> Name -> IO String`                                                                                                                                   |
36//! | `lean_rs_host_env_declaration_kind_bulk`               | yes        | `Environment -> Array String -> IO (Array String)`                                                                                                                   |
37//! | `lean_rs_host_env_declaration_kind_bulk_progress`      | yes        | `Environment -> Array String -> USize -> USize -> IO (Except UInt8 (Array String))`                                                                                  |
38//! | `lean_rs_host_env_declaration_name`                    | yes        | `Environment -> Name -> IO String`                                                                                                                                   |
39//! | `lean_rs_host_env_declaration_name_bulk`               | yes        | `Environment -> Array String -> IO (Array String)`                                                                                                                   |
40//! | `lean_rs_host_env_declaration_name_bulk_progress`      | yes        | `Environment -> Array String -> USize -> USize -> IO (Except UInt8 (Array String))`                                                                                  |
41//! | `lean_rs_host_env_search_declarations`                 | yes        | `Environment -> DeclarationSearchRequest -> Array String -> IO DeclarationSearchResult`                                                                              |
42//! | `lean_rs_host_env_inspect_declaration`                 | optional   | `Environment -> DeclarationInspectionRequest -> Array String -> IO DeclarationInspectionResult`                                                                       |
43//! | `lean_rs_host_env_expr_to_string_raw`                  | yes        | `Expr -> String`                                                                                                                                                     |
44//! | `lean_rs_host_elaborate`                               | yes        | `Environment -> String -> Option Expr -> String -> String -> UInt64 -> USize -> IO (Except ElabFailure Expr)`                                                        |
45//! | `lean_rs_host_elaborate_bulk`                          | yes        | `Environment -> Array String -> String -> String -> UInt64 -> USize -> IO (Array (Except ElabFailure Expr))`                                                         |
46//! | `lean_rs_host_elaborate_bulk_progress`                 | yes        | `Environment -> Array String -> String -> String -> UInt64 -> USize -> USize -> USize -> IO (Except UInt8 (Array (Except ElabFailure Expr)))`                        |
47//! | `lean_rs_host_kernel_check`                            | yes        | `Environment -> String -> String -> String -> UInt64 -> USize -> IO KernelOutcome`                                                                                   |
48//! | `lean_rs_host_kernel_check_progress`                   | yes        | `Environment -> String -> String -> String -> UInt64 -> USize -> USize -> USize -> IO (Except UInt8 KernelOutcome)`                                                  |
49//! | `lean_rs_host_check_evidence`                          | yes        | `Environment -> Evidence -> IO EvidenceStatus`                                                                                                                       |
50//! | `lean_rs_host_evidence_summary`                        | yes        | `Environment -> Evidence -> IO ProofSummary`                                                                                                                         |
51//! | `lean_rs_host_meta_infer_type`                         | optional   | `Environment -> Expr -> UInt64 -> USize -> UInt8 -> IO (MetaResponse Expr)`                                                                                          |
52//! | `lean_rs_host_meta_whnf`                               | optional   | `Environment -> Expr -> UInt64 -> USize -> UInt8 -> IO (MetaResponse Expr)`                                                                                          |
53//! | `lean_rs_host_meta_heartbeat_burn`                     | optional   | `Environment -> Expr -> UInt64 -> USize -> UInt8 -> IO (MetaResponse Expr)`                                                                                          |
54//! | `lean_rs_host_meta_is_def_eq`                          | optional   | `Environment -> (Expr × Expr × UInt8) -> UInt64 -> USize -> UInt8 -> IO (MetaResponse Bool)`                                                                         |
55//! | `lean_rs_host_meta_pp_expr`                            | optional   | `Environment -> Expr -> UInt64 -> USize -> UInt8 -> IO (MetaResponse String)`                                                                                        |
56//! | `lean_rs_host_process_module_query`                    | optional   | `Environment -> String -> ModuleQuery -> String -> String -> UInt64 -> USize -> IO ModuleQueryOutcome`                                                               |
57//! | `lean_rs_host_process_module_query_batch`              | optional   | `Environment -> String -> Array ModuleQuerySelector -> ModuleQueryOutputBudgets -> String -> String -> UInt64 -> USize -> IO ModuleQueryBatchOutcome`                |
58//! | `lean_rs_host_process_module_query_batch_cached`       | optional   | `Environment -> String -> Array ModuleQuerySelector -> ModuleQueryOutputBudgets -> String -> String -> UInt64 -> USize -> String -> IO ModuleQueryBatchCachedOutcome`|
59//! | `lean_rs_host_clear_module_snapshot_cache`             | optional   | `Unit -> IO ModuleSnapshotCacheClearResult`                                                                                                                          |
60//!
61//! Missing **mandatory** symbols surface at `load_capabilities` as
62//! [`lean_rs::HostStage::Link`]—failures bind to the capability's load,
63//! not to the first query. Missing **optional** symbols degrade
64//! gracefully: [`LeanSession::run_meta`] returns
65//! [`crate::host::meta::LeanMetaResponse::Unsupported`] against a service whose
66//! binding did not resolve, [`LeanSession::process_module_query`]
67//! returns [`crate::host::process::ModuleQueryOutcome::Unsupported`],
68//! and the rest of the capability stays usable.
69//! The evidence-side pair (`check_evidence`, `evidence_summary`) is
70//! mandatory because any capability that produces a `LeanEvidence`
71//! handle via `kernel_check` must also be able to re-validate and
72//! summarize it: the missing-symbol case defines no recoverable
73//! caller behaviour, so the error is folded into capability load
74//! rather than into every call site.
75//!
76//! Capability contracts are extended additively over time: any future
77//! capability symbol becomes a new mandatory or optional row in the
78//! table above without renaming or removing existing ones.
79//!
80//! ## Per-session metrics
81//!
82//! Every [`LeanSession`] carries a [`SessionStats`] counter that
83//! accumulates dispatch events (one FFI call per typed query, plus
84//! per-item counts for the bulk methods) and the wall time spent inside
85//! `.call(...)`. Snapshot via [`LeanSession::stats`]; reset by dropping
86//! the session. `import` itself is **not** counted as a query FFI call
87//!—pool reuse vs. fresh import is tracked at the
88//! [`crate::host::pool::SessionPool`] level instead.
89//!
90//! ## Cancellation
91//!
92//! Every public method that can enter Lean accepts
93//! `Option<&LeanCancellationToken>`. `None` keeps the fastest path and,
94//! for bulk methods, keeps the single Lean-side bulk dispatch. `Some`
95//! checks the token before host-controlled FFI dispatches; cancellable
96//! bulk methods switch to per-item dispatch so they can also check
97//! between items. Cancellation is cooperative and cannot interrupt a
98//! Lean call already in progress.
99//!
100//! ## Progress
101//!
102//! Long-running session operations also accept
103//! `Option<&dyn LeanProgressSink>`. `None` allocates no callback handle
104//! and preserves the existing fast path. `Some(sink)` delivers
105//! phase-local [`crate::host::progress::LeanProgressEvent`] values on
106//! the Lean-bound worker thread. A progress sink must not call back into
107//! the same session.
108//!
109//! The Rust side passes the `.olean` search path (resolved by
110//! [`crate::host::lake::LakeProject`]) as the first argument to
111//! `lean_rs_host_session_import`; the Lean shim only has to call
112//! `Lean.initSearchPath` and `Lean.importModules` on it. Path-layout
113//! knowledge stays in Rust.
114//!
115//! ## Lifetime story
116//!
117//! - `LeanSession<'lean, 'c>` borrows `&'c LeanCapabilities<'lean, '_>`.
118//! - The session's owned `Obj<'lean>` is independent of `'c`; it carries
119//!   one Lean refcount on the imported environment, anchored to the
120//!   runtime.
121//! - `HostShimBindings<'lean, 'c>` borrows from the manifest-backed shim
122//!   capability owned by `LeanCapabilities`; its typed call handles live
123//!   exactly as long as the session borrow.
124
125// `run_meta` is `pub` but bounded on `lean_rs::abi::traits::{LeanAbi, TryFromLean}`.
126// `LeanAbi` is sealed-public; `TryFromLean` is `pub(crate)`. The bound is a
127// crate-internal compatibility requirement, not a downstream extension point
128// (the meta-service registry is closed by `host::meta::service`). Same
129// precedent as `module::exported.rs`.
130#![allow(private_bounds, private_interfaces)]
131
132use core::cell::Cell;
133use std::sync::Mutex;
134use std::time::Instant;
135
136use crate::host::cancellation::{LeanCancellationToken, check_cancellation};
137use crate::host::capabilities::LeanCapabilities;
138use crate::host::declaration_search::{
139    DeclarationInspectionRequest, DeclarationInspectionResult, DeclarationSearchRequest, DeclarationSearchResult,
140};
141use crate::host::elaboration::{LeanElabFailure, LeanElabOptions};
142use crate::host::evidence::{EvidenceStatus, LeanEvidence, LeanKernelOutcome, ProofSummary};
143use crate::host::meta::{LeanMetaOptions, LeanMetaResponse, LeanMetaService};
144use crate::host::process::{
145    DeclarationVerificationOutcome, DeclarationVerificationRequest, ModuleQuery, ModuleQueryBatchCachedOutcome,
146    ModuleQueryBatchOutcome, ModuleQueryCachePolicy, ModuleQueryOutcome, ModuleQueryOutputBudgets, ModuleQuerySelector,
147    ModuleSnapshotCacheClearResult, ProofAttemptOutcome, ProofAttemptRequest,
148};
149use crate::host::progress::{LeanProgressSink, ProgressBridge, report_progress};
150use crate::host::shim_bindings::{HostShimBindings, binding_error_to_lean_error};
151use lean_rs::Obj;
152use lean_rs::abi::structure::{alloc_ctor_with_objects, take_ctor_objects};
153use lean_rs::abi::traits::{IntoLean, LeanAbi, TryFromLean, conversion_error, sealed};
154#[cfg(doc)]
155use lean_rs::error::HostStage;
156use lean_rs::error::LeanResult;
157use lean_rs::{LeanDeclaration, LeanExpr, LeanName};
158
159// -- SessionStats: per-session dispatch metrics --------------------------
160
161/// Cumulative dispatch metrics for one [`LeanSession`].
162///
163/// Snapshot via [`LeanSession::stats`]. Each typed query method records
164/// one FFI call; the bulk methods also record the per-item batch
165/// size. `elapsed_ns` accumulates the wall time spent inside the inner
166/// `.call(...)` dispatch (measured with [`Instant::now`])—it excludes
167/// Rust-side argument marshaling, name lookup, and result decoding so
168/// the number is comparable across singular and bulk paths.
169///
170/// `import` is **not** counted: import vs. reuse is tracked at the
171/// [`crate::host::pool::SessionPool`] level. Construction of a session
172/// always pays for one import, and that cost is reported by
173/// [`crate::host::pool::PoolStats::imports_performed`] when the session
174/// flows through a pool.
175#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
176pub struct SessionStats {
177    /// Number of typed query FFI calls dispatched through this session,
178    /// counting each singular call once and each bulk call once
179    /// regardless of batch size.
180    pub ffi_calls: u64,
181    /// Cumulative number of per-item entries processed by the bulk
182    /// methods. Singular calls do not contribute. A batch of N items
183    /// adds N here and 1 to [`Self::ffi_calls`].
184    pub batch_items: u64,
185    /// Cumulative nanoseconds spent inside the dispatch `.call(...)`
186    /// across every recorded FFI call.
187    pub elapsed_ns: u64,
188}
189
190// -- Public source-range / filter types ---------------------------------
191
192/// Source range Lean recorded for a declaration.
193///
194/// Coordinates are 1-based at every layer, matching the public
195/// convention of Lean declaration ranges. `file` is the path or module
196/// label Lean/Rust could resolve for the declaration; it is a label for
197/// consumers, not a normalized filesystem guarantee.
198#[derive(Clone, Debug, Eq, PartialEq)]
199pub struct LeanSourceRange {
200    /// File path or module label recorded for the declaration.
201    pub file: String,
202    /// 1-based start line.
203    pub start_line: u32,
204    /// 1-based start column.
205    pub start_column: u32,
206    /// 1-based end line.
207    pub end_line: u32,
208    /// 1-based end column.
209    pub end_column: u32,
210}
211
212impl<'lean> TryFromLean<'lean> for LeanSourceRange {
213    fn try_from_lean(obj: Obj<'lean>) -> LeanResult<Self> {
214        let [file_o, start_line_o, start_column_o, end_line_o, end_column_o] =
215            take_ctor_objects::<5>(obj, 0, "SourceRange")?;
216        Ok(Self {
217            file: String::try_from_lean(file_o)?,
218            start_line: u32::try_from_lean(start_line_o)?,
219            start_column: u32::try_from_lean(start_column_o)?,
220            end_line: u32::try_from_lean(end_line_o)?,
221            end_column: u32::try_from_lean(end_column_o)?,
222        })
223    }
224}
225
226/// Lean-side declaration-listing filter.
227///
228/// The default is tuned for user-facing declaration browsers: include
229/// private names because callers may be indexing the current project,
230/// but drop compiler-generated and internal-detail names that usually
231/// swamp the list with implementation artifacts.
232#[derive(Clone, Copy, Debug, Eq, PartialEq)]
233pub struct LeanDeclarationFilter {
234    /// Keep names Lean marks as private.
235    pub include_private: bool,
236    /// Keep generated names with numeric components.
237    pub include_generated: bool,
238    /// Keep Lean internal-detail names such as `_`, `match_`, `proof_`,
239    /// and similar implementation artifacts.
240    pub include_internal: bool,
241}
242
243impl Default for LeanDeclarationFilter {
244    fn default() -> Self {
245        Self {
246            include_private: true,
247            include_generated: false,
248            include_internal: false,
249        }
250    }
251}
252
253impl<'lean> IntoLean<'lean> for LeanDeclarationFilter {
254    fn into_lean(self, runtime: &'lean lean_rs::LeanRuntime) -> Obj<'lean> {
255        alloc_ctor_with_objects(
256            runtime,
257            0,
258            [
259                self.include_private.into_lean(runtime),
260                self.include_generated.into_lean(runtime),
261                self.include_internal.into_lean(runtime),
262            ],
263        )
264    }
265}
266
267impl<'lean> TryFromLean<'lean> for LeanDeclarationFilter {
268    fn try_from_lean(obj: Obj<'lean>) -> LeanResult<Self> {
269        let [include_private_o, include_generated_o, include_internal_o] =
270            take_ctor_objects::<3>(obj, 0, "DeclarationFilter")?;
271        Ok(Self {
272            include_private: bool::try_from_lean(include_private_o)?,
273            include_generated: bool::try_from_lean(include_generated_o)?,
274            include_internal: bool::try_from_lean(include_internal_o)?,
275        })
276    }
277}
278
279impl sealed::SealedAbi for LeanDeclarationFilter {}
280
281impl<'lean> LeanAbi<'lean> for LeanDeclarationFilter {
282    type CRepr = <Obj<'lean> as LeanAbi<'lean>>::CRepr;
283
284    fn into_c(self, runtime: &'lean lean_rs::LeanRuntime) -> Self::CRepr {
285        self.into_lean(runtime).into_raw()
286    }
287
288    fn from_c(_c: Self::CRepr, _runtime: &'lean lean_rs::LeanRuntime) -> LeanResult<Self> {
289        Err(conversion_error(
290            "LeanDeclarationFilter cannot decode a Lean call result; it is an argument-only type",
291        ))
292    }
293}
294
295// -- LeanSession ---------------------------------------------------------
296
297/// A long-lived Lean session over an imported environment.
298///
299/// Construct via [`LeanCapabilities::session`]. The session owns the
300/// imported `Lean.Environment` privately (never exposed) and dispatches
301/// each typed query through checked host-shim bindings resolved during
302/// construction. Neither [`Send`] nor [`Sync`]: inherited from the
303/// contained `Obj<'lean>` and the borrow of `LeanCapabilities`.
304pub struct LeanSession<'lean, 'c> {
305    capabilities: &'c LeanCapabilities<'lean, 'c>,
306    shims: HostShimBindings<'lean, 'c>,
307    /// The imported `Lean.Environment`. Private—Rust never inspects
308    /// the environment directly; every query routes through a Lean
309    /// capability export.
310    environment: Obj<'lean>,
311    /// Per-session dispatch metrics. `Cell` because every query method
312    /// takes `&mut self` but the bulk path can also be invoked through a
313    /// shared reference (e.g. inside a fold helper)—keeping the
314    /// counter in `Cell` makes the recording uniform without adding an
315    /// extra `&mut` borrow at each call site.
316    stats: Cell<SessionStats>,
317}
318
319/// Process-wide serialization for [`LeanSession::import`]. See the
320/// comment at the lock-acquire site for the Lean-4.30 race it closes.
321static SESSION_IMPORT_LOCK: Mutex<()> = Mutex::new(());
322
323impl<'lean, 'c> LeanSession<'lean, 'c> {
324    /// Import the named modules into a fresh Lean environment and wrap
325    /// it as a session.
326    ///
327    /// The Lean-side `lean_rs_host_session_import` receives the Lake
328    /// project root (so it can `Lean.initSearchPath` the `.olean`
329    /// directory) and the module-name list, and returns the resulting
330    /// environment. Failures surface as
331    /// [`lean_rs::LeanError::LeanException`] with the message Lean produced.
332    pub(crate) fn import(
333        capabilities: &'c LeanCapabilities<'lean, 'c>,
334        imports: &[&str],
335        cancellation: Option<&LeanCancellationToken>,
336        progress: Option<&dyn LeanProgressSink>,
337    ) -> LeanResult<Self> {
338        let _span = tracing::info_span!(
339            target: "lean_rs",
340            "lean_rs.host.session.import",
341            imports_len = imports.len(),
342        )
343        .entered();
344        check_cancellation(cancellation)?;
345        let project = capabilities.host().project();
346        let mut search_paths: Vec<String> = project
347            .olean_search_paths()
348            .into_iter()
349            .map(|path| path.to_string_lossy().into_owned())
350            .collect();
351        search_paths.push(
352            crate::host::lake::LakeProject::interop_olean_search_path()?
353                .to_string_lossy()
354                .into_owned(),
355        );
356        search_paths.push(
357            crate::host::lake::LakeProject::shim_olean_search_path()?
358                .to_string_lossy()
359                .into_owned(),
360        );
361        let imports_owned: Vec<String> = imports.iter().map(|&s| s.to_owned()).collect();
362        // Lean 4.30 strictly enforces `enableInitializersExecution` before
363        // `importModules (loadExts := true)`. The flag is process-global,
364        // but `Lean.withImporting` (wrapped around every import) resets it
365        // on completion—two threads importing concurrently race the
366        // shim's enable→import sequence and the loser sees the flag
367        // cleared by the winner's reset. Serializing the import phase
368        // across the process matches Lean's "single execution thread
369        // accessing the global references" requirement. Sessions operate
370        // concurrently on their own `Environment` values once import
371        // returns; the lock spans only the FFI call.
372        let _import_guard = SESSION_IMPORT_LOCK
373            .lock()
374            .unwrap_or_else(|poisoned| poisoned.into_inner());
375        let shims = HostShimBindings::resolve(capabilities.shim_capability())
376            .map_err(|err| binding_error_to_lean_error(&err))?;
377        let environment = if let Some(sink) = progress {
378            let bridge = ProgressBridge::new(sink, "import", Some(u64::try_from(imports.len()).unwrap_or(u64::MAX)))?;
379            let (handle, trampoline) = bridge.abi_parts();
380            let raw = shims
381                .session_import_progress
382                .call(search_paths, imports_owned, handle, trampoline)?;
383            bridge.decode(raw)?
384        } else {
385            shims.session_import.call(search_paths, imports_owned)?
386        };
387        Ok(Self {
388            capabilities,
389            shims,
390            environment,
391            stats: Cell::new(SessionStats::default()),
392        })
393    }
394
395    /// Wrap a previously-imported `Lean.Environment` as a fresh
396    /// [`LeanSession`] over `capabilities`.
397    ///
398    /// Crate-private; only [`crate::host::pool::SessionPool::acquire`]
399    /// calls this to recycle a pooled environment under a new
400    /// capability borrow. The returned session's [`SessionStats`] start
401    /// at zero—accumulated counters from the previous owner do not
402    /// leak across pool checkouts.
403    pub(crate) fn from_environment(
404        capabilities: &'c LeanCapabilities<'lean, 'c>,
405        environment: Obj<'lean>,
406    ) -> LeanResult<Self> {
407        let shims = HostShimBindings::resolve(capabilities.shim_capability())
408            .map_err(|err| binding_error_to_lean_error(&err))?;
409        Ok(Self {
410            capabilities,
411            shims,
412            environment,
413            stats: Cell::new(SessionStats::default()),
414        })
415    }
416
417    /// Consume the session and return its owned `Lean.Environment`.
418    ///
419    /// Crate-private; only [`crate::host::pool::SessionPool`] uses this
420    /// to reclaim the environment when a [`crate::host::pool::PooledSession`]
421    /// drops. The returned `Obj<'lean>` carries one Lean refcount, which
422    /// the pool is responsible for either pushing back into the free
423    /// list (in which case `Drop` runs later when the pool itself
424    /// drops) or releasing immediately (when at capacity).
425    pub(crate) fn into_environment(self) -> Obj<'lean> {
426        self.environment
427    }
428
429    /// Snapshot of this session's accumulated dispatch metrics.
430    ///
431    /// Returns a copy; the counters keep accumulating after the call.
432    /// Use [`SessionStats::default`] to compute a delta across two
433    /// snapshots.
434    #[must_use]
435    pub fn stats(&self) -> SessionStats {
436        self.stats.get()
437    }
438
439    /// Internal helper: record one FFI call and add `batch` per-item
440    /// entries plus `elapsed` nanoseconds. Singular methods pass
441    /// `batch = 0`; bulk methods pass the input length.
442    fn record_call(&self, batch: u64, elapsed: std::time::Duration) {
443        let mut s = self.stats.get();
444        s.ffi_calls = s.ffi_calls.saturating_add(1);
445        s.batch_items = s.batch_items.saturating_add(batch);
446        s.elapsed_ns = s
447            .elapsed_ns
448            .saturating_add(u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX));
449        self.stats.set(s);
450    }
451
452    fn decode_strings_cached(raw: Vec<Obj<'lean>>) -> LeanResult<Vec<String>> {
453        if raw.is_empty() {
454            return Ok(Vec::new());
455        }
456        let Some(first_key) = raw.first().map(Obj::as_raw_borrowed) else {
457            return Ok(Vec::new());
458        };
459        if raw.iter().all(|obj| obj.as_raw_borrowed() == first_key) {
460            let len = raw.len();
461            let mut raw_iter = raw.into_iter();
462            let Some(first) = raw_iter.next() else {
463                return Ok(Vec::new());
464            };
465            let value = String::try_from_lean(first)?;
466            return Ok(vec![value; len]);
467        }
468        let mut out = Vec::with_capacity(raw.len());
469        for obj in raw {
470            out.push(String::try_from_lean(obj)?);
471        }
472        Ok(out)
473    }
474
475    fn all_equal_name<'a>(names: &'a [&str]) -> Option<&'a str> {
476        let first = *names.first()?;
477        names.iter().all(|name| *name == first).then_some(first)
478    }
479
480    /// Look up a declaration by full Lean name (e.g. `"Nat.zero"`).
481    ///
482    /// # Errors
483    ///
484    /// Returns [`lean_rs::LeanError::Host`] with stage [`HostStage::Conversion`]
485    /// if the name is not present in the imported environment. Returns
486    /// [`lean_rs::LeanError::LeanException`] if the Lean-side query raises.
487    pub fn query_declaration(
488        &mut self,
489        name: &str,
490        cancellation: Option<&LeanCancellationToken>,
491    ) -> LeanResult<LeanDeclaration<'lean>> {
492        let _span = tracing::debug_span!(
493            target: "lean_rs",
494            "lean_rs.host.session.query_declaration",
495            name = name,
496        )
497        .entered();
498        check_cancellation(cancellation)?;
499        let name_handle = self.make_name(name, cancellation)?;
500        check_cancellation(cancellation)?;
501        let t = Instant::now();
502        let result = self
503            .shims
504            .env_query_declaration
505            .call(self.environment.clone(), name_handle);
506        self.record_call(0, t.elapsed());
507        match result? {
508            Some(decl) => Ok(decl),
509            None => Err(lean_rs::abi::traits::conversion_error(format!(
510                "declaration '{name}' not found in imported environment"
511            ))),
512        }
513    }
514
515    /// All declaration names in the imported environment.
516    ///
517    /// Returns a Vec; the environment's `constants` map contains many
518    /// thousands of entries even for a small project (Lean prelude is
519    /// always imported), so prefer [`LeanSession::query_declaration`]
520    /// when you already know the name.
521    ///
522    /// # Errors
523    ///
524    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side query
525    /// raises.
526    pub fn list_declarations(
527        &mut self,
528        cancellation: Option<&LeanCancellationToken>,
529    ) -> LeanResult<Vec<LeanName<'lean>>> {
530        let _span = tracing::debug_span!(
531            target: "lean_rs",
532            "lean_rs.host.session.list_declarations",
533        )
534        .entered();
535        check_cancellation(cancellation)?;
536        let t = Instant::now();
537        let raw = self.shims.env_list_declarations.call(self.environment.clone());
538        self.record_call(0, t.elapsed());
539        raw?.into_iter().map(LeanName::try_from_lean).collect()
540    }
541
542    /// Declaration names matching `filter`.
543    ///
544    /// Filtering runs inside Lean while traversing the environment
545    /// constants table, so Rust only allocates handles for names the
546    /// caller asked to keep.
547    ///
548    /// # Errors
549    ///
550    /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
551    /// already cancelled before dispatch. Returns
552    /// [`lean_rs::LeanError::LeanException`] if the Lean-side query
553    /// raises.
554    pub fn list_declarations_filtered(
555        &mut self,
556        filter: &LeanDeclarationFilter,
557        cancellation: Option<&LeanCancellationToken>,
558        progress: Option<&dyn LeanProgressSink>,
559    ) -> LeanResult<Vec<LeanName<'lean>>> {
560        let _span = tracing::debug_span!(
561            target: "lean_rs",
562            "lean_rs.host.session.list_declarations_filtered",
563            include_private = filter.include_private,
564            include_generated = filter.include_generated,
565            include_internal = filter.include_internal,
566        )
567        .entered();
568        check_cancellation(cancellation)?;
569        let raw = if let Some(sink) = progress {
570            let bridge = ProgressBridge::new(sink, "list_declarations_filtered", None)?;
571            let (handle, trampoline) = bridge.abi_parts();
572            let t = Instant::now();
573            let result = self.shims.env_list_declarations_filtered_progress.call(
574                self.environment.clone(),
575                *filter,
576                handle,
577                trampoline,
578            );
579            self.record_call(0, t.elapsed());
580            bridge.decode::<Vec<Obj<'lean>>>(result?)?
581        } else {
582            let t = Instant::now();
583            let result = self
584                .shims
585                .env_list_declarations_filtered
586                .call(self.environment.clone(), *filter);
587            self.record_call(0, t.elapsed());
588            result?
589        };
590        raw.into_iter().map(LeanName::try_from_lean).collect()
591    }
592
593    /// Source range Lean recorded for `name`.
594    ///
595    /// Returns `Ok(None)` when the name is absent or Lean has no
596    /// declaration range for it. That is normal for synthetic,
597    /// runtime-created, and some compiler-generated declarations.
598    ///
599    /// # Errors
600    ///
601    /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
602    /// already cancelled before dispatch. Returns
603    /// [`lean_rs::LeanError::LeanException`] if the Lean-side query
604    /// raises.
605    pub fn declaration_source_range(
606        &mut self,
607        name: &str,
608        cancellation: Option<&LeanCancellationToken>,
609    ) -> LeanResult<Option<LeanSourceRange>> {
610        let _span = tracing::debug_span!(
611            target: "lean_rs",
612            "lean_rs.host.session.declaration_source_range",
613            name = name,
614        )
615        .entered();
616        check_cancellation(cancellation)?;
617        let name_handle = self.make_name(name, cancellation)?;
618        check_cancellation(cancellation)?;
619        let source_roots = self
620            .capabilities
621            .host()
622            .project()
623            .source_roots()?
624            .into_iter()
625            .map(|path| path.to_string_lossy().into_owned())
626            .collect::<Vec<_>>();
627        check_cancellation(cancellation)?;
628        let t = Instant::now();
629        let result = self
630            .shims
631            .env_declaration_source_range
632            .call(self.environment.clone(), name_handle, source_roots);
633        self.record_call(0, t.elapsed());
634        result
635    }
636
637    /// The declared type of `name`, as an opaque [`LeanExpr`] handle.
638    ///
639    /// Returns `Ok(None)` if the name is not present in the environment.
640    ///
641    /// # Errors
642    ///
643    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side query
644    /// raises.
645    pub fn declaration_type(
646        &mut self,
647        name: &str,
648        cancellation: Option<&LeanCancellationToken>,
649    ) -> LeanResult<Option<LeanExpr<'lean>>> {
650        let _span = tracing::debug_span!(
651            target: "lean_rs",
652            "lean_rs.host.session.declaration_type",
653            name = name,
654        )
655        .entered();
656        check_cancellation(cancellation)?;
657        let name_handle = self.make_name(name, cancellation)?;
658        check_cancellation(cancellation)?;
659        let t = Instant::now();
660        let result = self
661            .shims
662            .env_declaration_type
663            .call(self.environment.clone(), name_handle);
664        self.record_call(0, t.elapsed());
665        result
666    }
667
668    /// The declared types of `names`, preserving input order.
669    ///
670    /// Returns `None` in each slot whose name is not present in the
671    /// environment. With `cancellation = None`, the whole batch crosses
672    /// the FFI boundary once and Lean converts the input strings to
673    /// names internally. With `Some(token)`, this loops through
674    /// [`Self::declaration_type`] so cancellation can be observed
675    /// between items; partial results are discarded when cancellation
676    /// fires.
677    ///
678    /// # Errors
679    ///
680    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side
681    /// bulk shim raises through `IO`.
682    pub fn declaration_type_bulk(
683        &mut self,
684        names: &[&str],
685        cancellation: Option<&LeanCancellationToken>,
686        progress: Option<&dyn LeanProgressSink>,
687    ) -> LeanResult<Vec<Option<LeanExpr<'lean>>>> {
688        let _span = tracing::debug_span!(
689            target: "lean_rs",
690            "lean_rs.host.session.declaration_type_bulk",
691            batch_size = names.len(),
692        )
693        .entered();
694        if names.is_empty() {
695            return Ok(Vec::new());
696        }
697        check_cancellation(cancellation)?;
698        if cancellation.is_some() {
699            let started = Instant::now();
700            let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
701            let mut out = Vec::with_capacity(names.len());
702            for (idx, name) in names.iter().enumerate() {
703                check_cancellation(cancellation)?;
704                out.push(self.declaration_type(name, cancellation)?);
705                report_progress(
706                    progress,
707                    "declaration_type_bulk",
708                    u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
709                    total,
710                    started,
711                )?;
712            }
713            return Ok(out);
714        }
715        if progress.is_none()
716            && let Some(name) = Self::all_equal_name(names)
717        {
718            let names_owned = vec![name.to_owned()];
719            let t = Instant::now();
720            let mut result = self
721                .shims
722                .env_declaration_type_bulk
723                .call(self.environment.clone(), names_owned)?;
724            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
725            self.record_call(batch_len, t.elapsed());
726            let value = result.pop().unwrap_or(None);
727            return Ok(vec![value; names.len()]);
728        }
729        let names_owned: Vec<String> = names.iter().map(|&name| name.to_owned()).collect();
730        if let Some(sink) = progress {
731            let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
732            let bridge = ProgressBridge::new(sink, "declaration_type_bulk", total)?;
733            let (handle, trampoline) = bridge.abi_parts();
734            let t = Instant::now();
735            let result = self.shims.env_declaration_type_bulk_progress.call(
736                self.environment.clone(),
737                names_owned,
738                handle,
739                trampoline,
740            );
741            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
742            self.record_call(batch_len, t.elapsed());
743            bridge.decode(result?)
744        } else {
745            let t = Instant::now();
746            let result = self
747                .shims
748                .env_declaration_type_bulk
749                .call(self.environment.clone(), names_owned);
750            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
751            self.record_call(batch_len, t.elapsed());
752            result
753        }
754    }
755
756    /// The kind of `name` as a Lean-rendered string
757    /// (`"axiom"`, `"definition"`, `"theorem"`, `"opaque"`, `"quot"`,
758    /// `"inductive"`, `"constructor"`, `"recursor"`), or `"missing"`
759    /// if `name` is not in the environment.
760    ///
761    /// # Errors
762    ///
763    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side query
764    /// raises.
765    pub fn declaration_kind(&mut self, name: &str, cancellation: Option<&LeanCancellationToken>) -> LeanResult<String> {
766        let _span = tracing::debug_span!(
767            target: "lean_rs",
768            "lean_rs.host.session.declaration_kind",
769            name = name,
770        )
771        .entered();
772        check_cancellation(cancellation)?;
773        let name_handle = self.make_name(name, cancellation)?;
774        check_cancellation(cancellation)?;
775        let t = Instant::now();
776        let result = self
777            .shims
778            .env_declaration_kind
779            .call(self.environment.clone(), name_handle);
780        self.record_call(0, t.elapsed());
781        result
782    }
783
784    /// The declaration kinds of `names`, preserving input order.
785    ///
786    /// Each output slot is the same string that [`Self::declaration_kind`]
787    /// would return for the corresponding input, including `"missing"`
788    /// for absent declarations. With `cancellation = None`, this is one
789    /// Lean-side bulk dispatch over an `Array String`; with
790    /// `Some(token)`, this loops through the singular path so the token
791    /// can be checked between items.
792    ///
793    /// # Errors
794    ///
795    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side
796    /// bulk shim raises through `IO`.
797    pub fn declaration_kind_bulk(
798        &mut self,
799        names: &[&str],
800        cancellation: Option<&LeanCancellationToken>,
801        progress: Option<&dyn LeanProgressSink>,
802    ) -> LeanResult<Vec<String>> {
803        let _span = tracing::debug_span!(
804            target: "lean_rs",
805            "lean_rs.host.session.declaration_kind_bulk",
806            batch_size = names.len(),
807        )
808        .entered();
809        if names.is_empty() {
810            return Ok(Vec::new());
811        }
812        check_cancellation(cancellation)?;
813        if cancellation.is_some() {
814            let started = Instant::now();
815            let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
816            let mut out = Vec::with_capacity(names.len());
817            for (idx, name) in names.iter().enumerate() {
818                check_cancellation(cancellation)?;
819                out.push(self.declaration_kind(name, cancellation)?);
820                report_progress(
821                    progress,
822                    "declaration_kind_bulk",
823                    u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
824                    total,
825                    started,
826                )?;
827            }
828            return Ok(out);
829        }
830        if progress.is_none()
831            && let Some(name) = Self::all_equal_name(names)
832        {
833            let names_owned = vec![name.to_owned()];
834            let t = Instant::now();
835            let mut result = Self::decode_strings_cached(
836                self.shims
837                    .env_declaration_kind_bulk
838                    .call(self.environment.clone(), names_owned)?,
839            )?;
840            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
841            self.record_call(batch_len, t.elapsed());
842            let value = result.pop().unwrap_or_default();
843            return Ok(vec![value; names.len()]);
844        }
845        let names_owned: Vec<String> = names.iter().map(|&name| name.to_owned()).collect();
846        if let Some(sink) = progress {
847            let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
848            let bridge = ProgressBridge::new(sink, "declaration_kind_bulk", total)?;
849            let (handle, trampoline) = bridge.abi_parts();
850            let t = Instant::now();
851            let result = self.shims.env_declaration_kind_bulk_progress.call(
852                self.environment.clone(),
853                names_owned,
854                handle,
855                trampoline,
856            );
857            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
858            self.record_call(batch_len, t.elapsed());
859            let raw = bridge.decode::<Vec<Obj<'lean>>>(result?)?;
860            Self::decode_strings_cached(raw)
861        } else {
862            let t = Instant::now();
863            let result = self
864                .shims
865                .env_declaration_kind_bulk
866                .call(self.environment.clone(), names_owned);
867            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
868            self.record_call(batch_len, t.elapsed());
869            Self::decode_strings_cached(result?)
870        }
871    }
872
873    /// The Lean-rendered display string of `name`. Round-trips a name
874    /// through the capability's `Name.toString` shim so callers see the
875    /// same canonical form Lean would log.
876    ///
877    /// Diagnostic only—not a semantic key. Use
878    /// [`LeanSession::query_declaration`] + a typed handle when
879    /// equality matters.
880    ///
881    /// # Errors
882    ///
883    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side query
884    /// raises.
885    pub fn declaration_name(&mut self, name: &str, cancellation: Option<&LeanCancellationToken>) -> LeanResult<String> {
886        let _span = tracing::debug_span!(
887            target: "lean_rs",
888            "lean_rs.host.session.declaration_name",
889            name = name,
890        )
891        .entered();
892        check_cancellation(cancellation)?;
893        let name_handle = self.make_name(name, cancellation)?;
894        check_cancellation(cancellation)?;
895        let t = Instant::now();
896        let result = self
897            .shims
898            .env_declaration_name
899            .call(self.environment.clone(), name_handle);
900        self.record_call(0, t.elapsed());
901        result
902    }
903
904    /// Lean-rendered display strings for `names`, preserving input
905    /// order.
906    ///
907    /// This is diagnostic text, not a semantic key. Missing
908    /// declarations are not an error because the singular
909    /// [`Self::declaration_name`] path also only round-trips the input
910    /// name through Lean's `Name.toString` renderer.
911    ///
912    /// With `cancellation = None`, this is one Lean-side bulk dispatch
913    /// over an `Array String`; with `Some(token)`, this loops through
914    /// the singular path so the token can be checked between items.
915    ///
916    /// # Errors
917    ///
918    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side
919    /// bulk shim raises through `IO`.
920    pub fn declaration_name_bulk(
921        &mut self,
922        names: &[&str],
923        cancellation: Option<&LeanCancellationToken>,
924        progress: Option<&dyn LeanProgressSink>,
925    ) -> LeanResult<Vec<String>> {
926        let _span = tracing::debug_span!(
927            target: "lean_rs",
928            "lean_rs.host.session.declaration_name_bulk",
929            batch_size = names.len(),
930        )
931        .entered();
932        if names.is_empty() {
933            return Ok(Vec::new());
934        }
935        check_cancellation(cancellation)?;
936        if cancellation.is_some() {
937            let started = Instant::now();
938            let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
939            let mut out = Vec::with_capacity(names.len());
940            for (idx, name) in names.iter().enumerate() {
941                check_cancellation(cancellation)?;
942                out.push(self.declaration_name(name, cancellation)?);
943                report_progress(
944                    progress,
945                    "declaration_name_bulk",
946                    u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
947                    total,
948                    started,
949                )?;
950            }
951            return Ok(out);
952        }
953        if progress.is_none()
954            && let Some(name) = Self::all_equal_name(names)
955        {
956            let names_owned = vec![name.to_owned()];
957            let t = Instant::now();
958            let mut result = Self::decode_strings_cached(
959                self.shims
960                    .env_declaration_name_bulk
961                    .call(self.environment.clone(), names_owned)?,
962            )?;
963            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
964            self.record_call(batch_len, t.elapsed());
965            let value = result.pop().unwrap_or_default();
966            return Ok(vec![value; names.len()]);
967        }
968        let names_owned: Vec<String> = names.iter().map(|&name| name.to_owned()).collect();
969        if let Some(sink) = progress {
970            let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
971            let bridge = ProgressBridge::new(sink, "declaration_name_bulk", total)?;
972            let (handle, trampoline) = bridge.abi_parts();
973            let t = Instant::now();
974            let result = self.shims.env_declaration_name_bulk_progress.call(
975                self.environment.clone(),
976                names_owned,
977                handle,
978                trampoline,
979            );
980            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
981            self.record_call(batch_len, t.elapsed());
982            let raw = bridge.decode::<Vec<Obj<'lean>>>(result?)?;
983            Self::decode_strings_cached(raw)
984        } else {
985            let t = Instant::now();
986            let result = self
987                .shims
988                .env_declaration_name_bulk
989                .call(self.environment.clone(), names_owned);
990            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
991            self.record_call(batch_len, t.elapsed());
992            Self::decode_strings_cached(result?)
993        }
994    }
995
996    /// Render an opaque [`LeanName`] handle as its dotted-string form,
997    /// routed through the capability's `Name.toString` shim.
998    ///
999    /// This is the supported way to turn a `LeanName` (e.g. an element
1000    /// of [`Self::list_declarations_filtered`]'s result) into Rust text.
1001    /// The output is diagnostic—not a semantic key—and equality on
1002    /// the underlying `Lean.Name` still lives in Lean.
1003    ///
1004    /// # Errors
1005    ///
1006    /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
1007    /// already cancelled before dispatch.
1008    pub fn name_to_string(
1009        &mut self,
1010        name: &LeanName<'lean>,
1011        cancellation: Option<&LeanCancellationToken>,
1012    ) -> LeanResult<String> {
1013        let _span = tracing::debug_span!(target: "lean_rs", "lean_rs.host.session.name_to_string").entered();
1014        check_cancellation(cancellation)?;
1015        let t = Instant::now();
1016        let result = self.shims.name_to_string.call(name.clone());
1017        self.record_call(0, t.elapsed());
1018        result
1019    }
1020
1021    /// Render `names` as dotted-string forms, preserving input order.
1022    ///
1023    /// Implemented as a per-item loop over [`Self::name_to_string`] in
1024    /// v1: cancellation is checked between items, progress is reported
1025    /// after each. The Lean shim is pure and short, so the per-item FFI
1026    /// overhead is acceptable; a bulk shim is a future optimisation if
1027    /// profiling shows it matters.
1028    ///
1029    /// # Errors
1030    ///
1031    /// Returns [`lean_rs::LeanError::Cancelled`] between items if the
1032    /// token is tripped during the walk.
1033    pub fn name_to_string_bulk(
1034        &mut self,
1035        names: &[LeanName<'lean>],
1036        cancellation: Option<&LeanCancellationToken>,
1037        progress: Option<&dyn LeanProgressSink>,
1038    ) -> LeanResult<Vec<String>> {
1039        let _span = tracing::debug_span!(
1040            target: "lean_rs",
1041            "lean_rs.host.session.name_to_string_bulk",
1042            batch_size = names.len(),
1043        )
1044        .entered();
1045        if names.is_empty() {
1046            return Ok(Vec::new());
1047        }
1048        check_cancellation(cancellation)?;
1049        let started = Instant::now();
1050        let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1051        let mut out = Vec::with_capacity(names.len());
1052        for (idx, name) in names.iter().enumerate() {
1053            check_cancellation(cancellation)?;
1054            out.push(self.name_to_string(name, cancellation)?);
1055            report_progress(
1056                progress,
1057                "name_to_string_bulk",
1058                u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
1059                total,
1060                started,
1061            )?;
1062        }
1063        Ok(out)
1064    }
1065
1066    /// Enumerate the imported environment's declaration names and render
1067    /// each as a dotted string. Convenience over
1068    /// [`Self::list_declarations_filtered`] + [`Self::name_to_string_bulk`]
1069    /// for the common case where the consumer only needs strings.
1070    ///
1071    /// Two FFI hops (list + per-name render) and one heap allocation
1072    /// per name. For batches under a few thousand this is fine; for
1073    /// six-figure walks consider the lower-level pair so the listing
1074    /// pass and the rendering pass can be cancelled or chunked
1075    /// independently.
1076    ///
1077    /// # Errors
1078    ///
1079    /// Forwards errors from [`Self::list_declarations_filtered`] and
1080    /// [`Self::name_to_string_bulk`].
1081    pub fn list_declarations_strings(
1082        &mut self,
1083        filter: &LeanDeclarationFilter,
1084        cancellation: Option<&LeanCancellationToken>,
1085        progress: Option<&dyn LeanProgressSink>,
1086    ) -> LeanResult<Vec<String>> {
1087        let _span = tracing::debug_span!(target: "lean_rs", "lean_rs.host.session.list_declarations_strings").entered();
1088        let names = self.list_declarations_filtered(filter, cancellation, None)?;
1089        self.name_to_string_bulk(&names, cancellation, progress)
1090    }
1091
1092    /// Search declarations with structural filters and bounded metadata rows.
1093    ///
1094    /// The search runs inside Lean while traversing the imported environment.
1095    /// It may inspect declaration types structurally for required constants and
1096    /// conclusion heads, but it never renders type text. Use
1097    /// [`Self::declaration_type`] for explicit one-name type rendering.
1098    ///
1099    /// # Errors
1100    ///
1101    /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is already
1102    /// cancelled before dispatch. Returns [`lean_rs::LeanError::LeanException`]
1103    /// if the Lean-side search raises.
1104    pub fn search_declarations(
1105        &mut self,
1106        search: &DeclarationSearchRequest,
1107        cancellation: Option<&LeanCancellationToken>,
1108    ) -> LeanResult<DeclarationSearchResult> {
1109        let _span = tracing::debug_span!(
1110            target: "lean_rs",
1111            "lean_rs.host.session.search_declarations",
1112            limit = search.limit,
1113            include_source = search.include_source,
1114        )
1115        .entered();
1116        check_cancellation(cancellation)?;
1117        let source_roots = if search.include_source {
1118            self.capabilities
1119                .host()
1120                .project()
1121                .source_roots()?
1122                .into_iter()
1123                .map(|path| path.to_string_lossy().into_owned())
1124                .collect::<Vec<_>>()
1125        } else {
1126            Vec::new()
1127        };
1128        check_cancellation(cancellation)?;
1129        let t = Instant::now();
1130        let result = self
1131            .shims
1132            .env_search_declarations
1133            .call(self.environment.clone(), search.clone(), source_roots);
1134        self.record_call(0, t.elapsed());
1135        result
1136    }
1137
1138    /// Inspect one selected declaration under explicit output budgets.
1139    ///
1140    /// Search remains metadata-only; callers use this method after selecting
1141    /// one declaration name whose rendered statement/docstring are worth
1142    /// paying for. Missing names and missing optional shim support are normal
1143    /// result statuses.
1144    ///
1145    /// # Errors
1146    ///
1147    /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is already
1148    /// cancelled before dispatch. Returns [`lean_rs::LeanError::LeanException`]
1149    /// if the Lean-side inspection raises.
1150    pub fn inspect_declaration(
1151        &mut self,
1152        request: &DeclarationInspectionRequest,
1153        cancellation: Option<&LeanCancellationToken>,
1154    ) -> LeanResult<DeclarationInspectionResult> {
1155        let _span = tracing::debug_span!(
1156            target: "lean_rs",
1157            "lean_rs.host.session.inspect_declaration",
1158            source = request.fields.source,
1159            statement = request.fields.statement,
1160            docstring = request.fields.docstring,
1161            attributes = request.fields.attributes,
1162            flags = request.fields.flags,
1163        )
1164        .entered();
1165        check_cancellation(cancellation)?;
1166        let Some(inspect) = &self.shims.env_inspect_declaration else {
1167            return Ok(DeclarationInspectionResult::Unsupported);
1168        };
1169        let source_roots = if request.fields.source {
1170            self.capabilities
1171                .host()
1172                .project()
1173                .source_roots()?
1174                .into_iter()
1175                .map(|path| path.to_string_lossy().into_owned())
1176                .collect::<Vec<_>>()
1177        } else {
1178            Vec::new()
1179        };
1180        check_cancellation(cancellation)?;
1181        let t = Instant::now();
1182        // Bound the optional notation-aware statement rendering by the default
1183        // heartbeat budget; a deeply nested term that exceeds it falls back to
1184        // the raw `Expr.toString` form inside the shim.
1185        let result = inspect.call(
1186            self.environment.clone(),
1187            request.clone(),
1188            source_roots,
1189            lean_toolchain::LEAN_HEARTBEAT_LIMIT_DEFAULT,
1190        );
1191        self.record_call(0, t.elapsed());
1192        result
1193    }
1194
1195    /// Render `expr` via `Expr.toString`—the cheap, deterministic
1196    /// projection.
1197    ///
1198    /// Walks the syntax tree directly: no `MetaM`, no notation lookup,
1199    /// no binder pretty-printing. The result is a legible-but-ugly
1200    /// dump suitable for indexing, logging, and search keys. For the
1201    /// form a Lean user reads, use the optional
1202    /// [`crate::host::meta::pp_expr`] service through
1203    /// [`Self::run_meta`] instead—it pays for elaboration context to
1204    /// get notation and unfolding right but can time out under a tight
1205    /// heartbeat budget.
1206    ///
1207    /// # Errors
1208    ///
1209    /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
1210    /// already cancelled before dispatch.
1211    pub fn expr_to_string_raw(
1212        &mut self,
1213        expr: &LeanExpr<'lean>,
1214        cancellation: Option<&LeanCancellationToken>,
1215    ) -> LeanResult<String> {
1216        let _span = tracing::debug_span!(target: "lean_rs", "lean_rs.host.session.expr_to_string_raw").entered();
1217        check_cancellation(cancellation)?;
1218        let t = Instant::now();
1219        let result = self.shims.env_expr_to_string_raw.call(expr.clone());
1220        self.record_call(0, t.elapsed());
1221        result
1222    }
1223
1224    /// Parse and elaborate a Lean module, returning only the requested
1225    /// bounded projection.
1226    ///
1227    /// The Lean shim owns header parsing, module-system header handling,
1228    /// info-tree traversal, cursor selection, reference collection, and
1229    /// bounded expression/goal rendering. The Rust side chooses a
1230    /// [`ModuleQuery`] and receives the matching
1231    /// [`ModuleQueryOutcome`]; whole-file raw expression/type dumps never
1232    /// cross this boundary.
1233    ///
1234    /// The shim is optional. When the loaded capability dylib does not
1235    /// export `lean_rs_host_process_module_query`, the method returns
1236    /// [`ModuleQueryOutcome::Unsupported`] without an FFI call.
1237    ///
1238    /// # Errors
1239    ///
1240    /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
1241    /// already cancelled before dispatch. Returns
1242    /// [`lean_rs::LeanError::LeanException`] if the Lean-side shim
1243    /// raises through `IO`. Returns [`lean_rs::LeanError::Host`] with
1244    /// stage [`HostStage::Conversion`] if the Lean return value does
1245    /// not decode into [`ModuleQueryOutcome`].
1246    pub fn process_module_query(
1247        &mut self,
1248        source: &str,
1249        query: &ModuleQuery,
1250        options: &LeanElabOptions,
1251        cancellation: Option<&LeanCancellationToken>,
1252    ) -> LeanResult<ModuleQueryOutcome> {
1253        let _span = tracing::debug_span!(
1254            target: "lean_rs",
1255            "lean_rs.host.session.process_module_query",
1256            source_len = source.len(),
1257            heartbeats = options.heartbeats(),
1258            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1259        )
1260        .entered();
1261        check_cancellation(cancellation)?;
1262        let Some(call) = self.shims.process_module_query.as_ref() else {
1263            return Ok(ModuleQueryOutcome::Unsupported);
1264        };
1265        let t = Instant::now();
1266        let result = call.call(
1267            self.environment.clone(),
1268            source.to_owned(),
1269            query.clone(),
1270            options.namespace_context_str().to_owned(),
1271            options.file_label_str().to_owned(),
1272            options.heartbeats(),
1273            options.diagnostic_byte_limit_usize(),
1274        );
1275        self.record_call(0, t.elapsed());
1276        result
1277    }
1278
1279    /// Parse and elaborate a Lean module once, returning several bounded
1280    /// projections keyed by selector id.
1281    ///
1282    /// This is the proof-agent path: Lean owns header handling, one body
1283    /// elaboration, info-tree traversal, and selector projection. Rust sends
1284    /// a small selector array and receives per-selector outcomes; whole-file
1285    /// info-tree arrays never cross the boundary.
1286    ///
1287    /// The shim is optional. When the loaded capability dylib does not
1288    /// export `lean_rs_host_process_module_query_batch`, the method returns
1289    /// [`ModuleQueryBatchOutcome::Unsupported`] without an FFI call.
1290    ///
1291    /// # Errors
1292    ///
1293    /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
1294    /// already cancelled before dispatch. Returns
1295    /// [`lean_rs::LeanError::LeanException`] if the Lean-side shim raises
1296    /// through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
1297    /// [`HostStage::Conversion`] if the Lean return value does not decode
1298    /// into [`ModuleQueryBatchOutcome`].
1299    pub fn process_module_query_batch(
1300        &mut self,
1301        source: &str,
1302        selectors: &[ModuleQuerySelector],
1303        budgets: &ModuleQueryOutputBudgets,
1304        options: &LeanElabOptions,
1305        cancellation: Option<&LeanCancellationToken>,
1306    ) -> LeanResult<ModuleQueryBatchOutcome> {
1307        let _span = tracing::debug_span!(
1308            target: "lean_rs",
1309            "lean_rs.host.session.process_module_query_batch",
1310            source_len = source.len(),
1311            selectors = selectors.len(),
1312            per_field_bytes = budgets.per_field_bytes,
1313            total_bytes = budgets.total_bytes,
1314            heartbeats = options.heartbeats(),
1315            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1316        )
1317        .entered();
1318        check_cancellation(cancellation)?;
1319        let Some(call) = self.shims.process_module_query_batch.as_ref() else {
1320            return Ok(ModuleQueryBatchOutcome::Unsupported);
1321        };
1322        let selectors_owned = selectors.to_vec();
1323        let t = Instant::now();
1324        let result = call.call(
1325            self.environment.clone(),
1326            source.to_owned(),
1327            selectors_owned,
1328            budgets.clone(),
1329            options.namespace_context_str().to_owned(),
1330            options.file_label_str().to_owned(),
1331            options.heartbeats(),
1332            options.diagnostic_byte_limit_usize(),
1333        );
1334        self.record_call(u64::try_from(selectors.len()).unwrap_or(u64::MAX), t.elapsed());
1335        result
1336    }
1337
1338    /// Parse/elaborate a Lean module through the shim-owned module snapshot
1339    /// cache, then return bounded selector projections plus cache facts.
1340    ///
1341    /// The snapshot cache remains private to the loaded shim. Rust provides
1342    /// the stable cache key and conservative policy, but never receives raw
1343    /// info trees.
1344    ///
1345    /// # Errors
1346    ///
1347    /// Returns an error if cancellation is already requested, if the shim
1348    /// raises an `IO` exception, or if the Lean result cannot be decoded into
1349    /// the expected cached batch outcome.
1350    pub fn process_module_query_batch_cached(
1351        &mut self,
1352        source: &str,
1353        selectors: &[ModuleQuerySelector],
1354        budgets: &ModuleQueryOutputBudgets,
1355        options: &LeanElabOptions,
1356        policy: &ModuleQueryCachePolicy,
1357        cancellation: Option<&LeanCancellationToken>,
1358    ) -> LeanResult<ModuleQueryBatchCachedOutcome> {
1359        let _span = tracing::debug_span!(
1360            target: "lean_rs",
1361            "lean_rs.host.session.process_module_query_batch_cached",
1362            source_len = source.len(),
1363            selectors = selectors.len(),
1364            per_field_bytes = budgets.per_field_bytes,
1365            total_bytes = budgets.total_bytes,
1366            heartbeats = options.heartbeats(),
1367            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1368        )
1369        .entered();
1370        check_cancellation(cancellation)?;
1371        let Some(call) = self.shims.process_module_query_batch_cached.as_ref() else {
1372            return Ok(ModuleQueryBatchCachedOutcome::Unsupported);
1373        };
1374        let selectors_owned = selectors.to_vec();
1375        let policy_text = format!(
1376            "{}\n{}\n{}\n{}\n{}",
1377            policy.file_identity, policy.key, policy.max_entries, policy.ttl_millis, policy.max_bytes
1378        );
1379        let t = Instant::now();
1380        let result = call.call(
1381            self.environment.clone(),
1382            source.to_owned(),
1383            selectors_owned,
1384            budgets.clone(),
1385            options.namespace_context_str().to_owned(),
1386            options.file_label_str().to_owned(),
1387            options.heartbeats(),
1388            options.diagnostic_byte_limit_usize(),
1389            policy_text,
1390        );
1391        self.record_call(u64::try_from(selectors.len()).unwrap_or(u64::MAX), t.elapsed());
1392        result
1393    }
1394
1395    /// Try proof snippets against an in-memory source overlay.
1396    ///
1397    /// The shim is optional. When the loaded capability dylib does not export
1398    /// `lean_rs_host_attempt_proof`, the method returns
1399    /// [`ProofAttemptOutcome::Unsupported`] without an FFI call.
1400    ///
1401    /// # Errors
1402    ///
1403    /// Returns an error if cancellation is already requested, if the shim
1404    /// raises an `IO` exception, or if the Lean result cannot be decoded.
1405    pub fn attempt_proof(
1406        &mut self,
1407        request: &ProofAttemptRequest,
1408        options: &LeanElabOptions,
1409        cancellation: Option<&LeanCancellationToken>,
1410    ) -> LeanResult<ProofAttemptOutcome> {
1411        let _span = tracing::debug_span!(
1412            target: "lean_rs",
1413            "lean_rs.host.session.attempt_proof",
1414            source_len = request.source.len(),
1415            candidates = request.candidates.len(),
1416            per_field_bytes = request.budgets.per_field_bytes,
1417            total_bytes = request.budgets.total_bytes,
1418            heartbeats = options.heartbeats(),
1419            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1420        )
1421        .entered();
1422        check_cancellation(cancellation)?;
1423        let Some(call) = self.shims.attempt_proof.as_ref() else {
1424            return Ok(ProofAttemptOutcome::Unsupported);
1425        };
1426        let t = Instant::now();
1427        let result = call.call(
1428            self.environment.clone(),
1429            request.clone(),
1430            options.namespace_context_str().to_owned(),
1431            options.file_label_str().to_owned(),
1432            options.heartbeats(),
1433            options.diagnostic_byte_limit_usize(),
1434        );
1435        self.record_call(u64::try_from(request.candidates.len()).unwrap_or(u64::MAX), t.elapsed());
1436        result
1437    }
1438
1439    /// Verify one declaration in an in-memory source snapshot.
1440    ///
1441    /// The shim is optional. When the loaded capability dylib does not export
1442    /// `lean_rs_host_verify_declaration`, the method returns
1443    /// [`DeclarationVerificationOutcome::Unsupported`] without an FFI call.
1444    ///
1445    /// # Errors
1446    ///
1447    /// Returns an error if cancellation is already requested, if the shim
1448    /// raises an `IO` exception, or if the Lean result cannot be decoded.
1449    pub fn verify_declaration(
1450        &mut self,
1451        request: &DeclarationVerificationRequest,
1452        options: &LeanElabOptions,
1453        cancellation: Option<&LeanCancellationToken>,
1454    ) -> LeanResult<DeclarationVerificationOutcome> {
1455        let _span = tracing::debug_span!(
1456            target: "lean_rs",
1457            "lean_rs.host.session.verify_declaration",
1458            source_len = request.source.len(),
1459            report_axioms = request.report_axioms,
1460            per_field_bytes = request.budgets.per_field_bytes,
1461            total_bytes = request.budgets.total_bytes,
1462            heartbeats = options.heartbeats(),
1463            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1464        )
1465        .entered();
1466        check_cancellation(cancellation)?;
1467        let Some(call) = self.shims.verify_declaration.as_ref() else {
1468            return Ok(DeclarationVerificationOutcome::Unsupported);
1469        };
1470        let t = Instant::now();
1471        let result = call.call(
1472            self.environment.clone(),
1473            request.clone(),
1474            options.namespace_context_str().to_owned(),
1475            options.file_label_str().to_owned(),
1476            options.heartbeats(),
1477            options.diagnostic_byte_limit_usize(),
1478        );
1479        self.record_call(1, t.elapsed());
1480        result
1481    }
1482
1483    /// Clear the shim-owned module snapshot cache when the loaded capability
1484    /// supports it.
1485    ///
1486    /// # Errors
1487    ///
1488    /// Returns an error if the shim raises an `IO` exception or if the Lean
1489    /// clear result cannot be decoded.
1490    pub fn clear_module_snapshot_cache(&mut self) -> LeanResult<ModuleSnapshotCacheClearResult> {
1491        let Some(call) = self.shims.clear_module_snapshot_cache.as_ref() else {
1492            return Ok(ModuleSnapshotCacheClearResult {
1493                entries_cleared: 0,
1494                approx_bytes_cleared: 0,
1495            });
1496        };
1497        let t = Instant::now();
1498        let result = call.call();
1499        self.record_call(0, t.elapsed());
1500        result
1501    }
1502
1503    /// Parse and elaborate a single Lean term against the imported
1504    /// environment, optionally against an expected type.
1505    ///
1506    /// The boundary is explicit: Rust supplies the source text, module
1507    /// context, and bounded options; Lean parses, elaborates, and
1508    /// returns either an opaque [`LeanExpr`] handle or a structured
1509    /// [`LeanElabFailure`] carrying typed diagnostics. Rust does not
1510    /// inspect elaborator internals or proof terms to decide
1511    /// correctness.
1512    ///
1513    /// The outer [`LeanResult`] surfaces host-stack failures (a Lean
1514    /// `IO`-level exception from the shim itself, a malformed Lean
1515    /// return value); the inner `Result` distinguishes successful
1516    /// elaboration from parse / type / kernel-stage failures the
1517    /// elaborator reports through its `MessageLog`. Both error paths
1518    /// propagate the [`LeanElabOptions::diagnostic_byte_limit`] bound
1519    /// structurally.
1520    ///
1521    /// # Errors
1522    ///
1523    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side shim raises
1524    /// through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
1525    /// [`HostStage::Conversion`] if the Lean return value does not
1526    /// decode into [`LeanElabFailure`] / [`LeanExpr`].
1527    pub fn elaborate(
1528        &mut self,
1529        source: &str,
1530        expected_type: Option<&LeanExpr<'lean>>,
1531        options: &LeanElabOptions,
1532        cancellation: Option<&LeanCancellationToken>,
1533    ) -> LeanResult<Result<LeanExpr<'lean>, LeanElabFailure>> {
1534        let _span = tracing::debug_span!(
1535            target: "lean_rs",
1536            "lean_rs.host.session.elaborate",
1537            source_len = source.len(),
1538            heartbeats = options.heartbeats(),
1539            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1540        )
1541        .entered();
1542        check_cancellation(cancellation)?;
1543        let t = Instant::now();
1544        let result = self.shims.elaborate.call(
1545            self.environment.clone(),
1546            source.to_owned(),
1547            expected_type.cloned(),
1548            options.namespace_context_str().to_owned(),
1549            options.file_label_str().to_owned(),
1550            options.heartbeats(),
1551            options.diagnostic_byte_limit_usize(),
1552        );
1553        self.record_call(0, t.elapsed());
1554        result
1555    }
1556
1557    /// Parse, elaborate, and kernel-check a Lean declaration source
1558    /// (typically a `theorem` or `def`), returning a typed outcome
1559    /// that classifies the result and carries either the produced
1560    /// [`crate::LeanEvidence`] handle or the diagnostics the elaborator and
1561    /// kernel emitted.
1562    ///
1563    /// The boundary is explicit (mirrors [`Self::elaborate`]): Rust
1564    /// supplies source + options; Lean parses, elaborates, runs
1565    /// `addDecl` (which kernel-checks), and classifies the outcome.
1566    /// Rust never inspects the produced proof term or declaration
1567    /// internals.
1568    ///
1569    /// # Errors
1570    ///
1571    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side shim
1572    /// raises through `IO` (an unexpected internal failure that is not
1573    /// itself a rejection / unavailable diagnostic). Returns
1574    /// [`lean_rs::LeanError::Host`] with stage [`HostStage::Conversion`] if the
1575    /// Lean return value does not decode into [`LeanKernelOutcome`].
1576    pub fn kernel_check(
1577        &mut self,
1578        source: &str,
1579        options: &LeanElabOptions,
1580        cancellation: Option<&LeanCancellationToken>,
1581        progress: Option<&dyn LeanProgressSink>,
1582    ) -> LeanResult<LeanKernelOutcome<'lean>> {
1583        let _span = tracing::debug_span!(
1584            target: "lean_rs",
1585            "lean_rs.host.session.kernel_check",
1586            source_len = source.len(),
1587            heartbeats = options.heartbeats(),
1588            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1589        )
1590        .entered();
1591        check_cancellation(cancellation)?;
1592        if let Some(sink) = progress {
1593            let bridge = ProgressBridge::new(sink, "kernel_check", Some(1))?;
1594            let (handle, trampoline) = bridge.abi_parts();
1595            let t = Instant::now();
1596            let result = self.shims.kernel_check_progress.call(
1597                self.environment.clone(),
1598                source.to_owned(),
1599                options.namespace_context_str().to_owned(),
1600                options.file_label_str().to_owned(),
1601                options.heartbeats(),
1602                options.diagnostic_byte_limit_usize(),
1603                handle,
1604                trampoline,
1605            );
1606            self.record_call(0, t.elapsed());
1607            bridge.decode(result?)
1608        } else {
1609            let t = Instant::now();
1610            let result = self.shims.kernel_check.call(
1611                self.environment.clone(),
1612                source.to_owned(),
1613                options.namespace_context_str().to_owned(),
1614                options.file_label_str().to_owned(),
1615                options.heartbeats(),
1616                options.diagnostic_byte_limit_usize(),
1617            );
1618            self.record_call(0, t.elapsed());
1619            result
1620        }
1621    }
1622
1623    /// Re-validate a previously captured [`LeanEvidence`] against the
1624    /// session's imported environment, returning the kernel's current
1625    /// verdict.
1626    ///
1627    /// The handle was produced by an earlier
1628    /// [`Self::kernel_check`] call against this same environment and
1629    /// carries the kernel-accepted `Lean.Declaration` opaquely. The
1630    /// session never installs that declaration into its stored
1631    /// environment, so re-checking against the unchanged environment
1632    /// is the supported way to ask "is this evidence still valid?"—
1633    /// the kernel runs fresh.
1634    ///
1635    /// The returned [`EvidenceStatus`] mirrors
1636    /// [`LeanKernelOutcome::status`]: `Checked` on success, `Rejected`
1637    /// if the kernel now refuses the declaration, `Unavailable` if
1638    /// the Lean shim caught an `IO` exception. The Lean fixture does
1639    /// not currently emit `Unsupported` from this path—`Unsupported`
1640    /// only fires during the initial classification in
1641    /// `kernel_check`.
1642    ///
1643    /// # Errors
1644    ///
1645    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean shim raises
1646    /// through `IO` outside of its own `try` (an unexpected internal
1647    /// failure that the shim did not classify). Returns
1648    /// [`lean_rs::LeanError::Host`] with stage [`HostStage::Conversion`] if the
1649    /// return value does not decode as a four-tag
1650    /// [`EvidenceStatus`] inductive.
1651    pub fn check_evidence(
1652        &mut self,
1653        handle: &LeanEvidence<'lean>,
1654        cancellation: Option<&LeanCancellationToken>,
1655    ) -> LeanResult<EvidenceStatus> {
1656        let _span = tracing::debug_span!(
1657            target: "lean_rs",
1658            "lean_rs.host.session.check_evidence",
1659        )
1660        .entered();
1661        check_cancellation(cancellation)?;
1662        let t = Instant::now();
1663        let result = self.shims.check_evidence.call(self.environment.clone(), handle.clone());
1664        self.record_call(0, t.elapsed());
1665        result
1666    }
1667
1668    /// Project a previously captured [`LeanEvidence`] into a bounded
1669    /// [`ProofSummary`] for diagnostics or storage.
1670    ///
1671    /// The Lean shim renders the captured declaration's name, kind,
1672    /// and type expression as three byte-bounded `String`s—no
1673    /// `Lean.Expr` or proof term crosses the FFI boundary. The
1674    /// summary is computed on demand (not at
1675    /// [`Self::kernel_check`] time) because most callers only ever
1676    /// inspect the [`EvidenceStatus`] tag and would pay the
1677    /// pretty-print cost for nothing.
1678    ///
1679    /// Strings on the returned summary are display text. They are not
1680    /// semantic keys; route equality comparisons through a
1681    /// Lean-authored equality export.
1682    ///
1683    /// # Errors
1684    ///
1685    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean shim raises
1686    /// through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
1687    /// [`HostStage::Conversion`] if the return value does not decode
1688    /// as a three-field [`ProofSummary`] structure.
1689    pub fn summarize_evidence(
1690        &mut self,
1691        handle: &LeanEvidence<'lean>,
1692        cancellation: Option<&LeanCancellationToken>,
1693    ) -> LeanResult<ProofSummary> {
1694        let _span = tracing::debug_span!(
1695            target: "lean_rs",
1696            "lean_rs.host.session.summarize_evidence",
1697        )
1698        .entered();
1699        check_cancellation(cancellation)?;
1700        let t = Instant::now();
1701        let result = self
1702            .shims
1703            .evidence_summary
1704            .call(self.environment.clone(), handle.clone());
1705        self.record_call(0, t.elapsed());
1706        result
1707    }
1708
1709    /// Invoke a registered bounded [`MetaM`](https://leanprover.github.io/theorem_proving_in_lean4/)
1710    /// service against the imported environment.
1711    ///
1712    /// The session dispatches through the checked binding for the closed
1713    /// service shape; if the loaded capability does not export the optional
1714    /// symbol, the call short-circuits to [`LeanMetaResponse::Unsupported`]
1715    /// with a synthetic host-side diagnostic naming the missing symbol.
1716    ///
1717    /// The outer [`LeanResult`] surfaces host-stack failures (a Lean
1718    /// `IO`-level exception from the shim itself, or an undecodable
1719    /// return value). The four-way classification—`Ok` / `Failed` /
1720    /// `TimeoutOrHeartbeat` / `Unsupported`—lives in the inner
1721    /// [`LeanMetaResponse`].
1722    ///
1723    /// # Errors
1724    ///
1725    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean shim raises
1726    /// through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
1727    /// [`HostStage::Conversion`] if the return value does not decode
1728    /// into [`LeanMetaResponse<Resp>`].
1729    pub fn run_meta<Req, Resp>(
1730        &mut self,
1731        service: &LeanMetaService<Req, Resp>,
1732        request: Req,
1733        options: &LeanMetaOptions,
1734        cancellation: Option<&LeanCancellationToken>,
1735    ) -> LeanResult<LeanMetaResponse<Resp>>
1736    where
1737        LeanMetaService<Req, Resp>: HostMetaDispatch<'lean, Req, Resp>,
1738        Req: lean_rs::abi::traits::LeanAbi<'lean>,
1739        Resp: TryFromLean<'lean>,
1740    {
1741        let _span = tracing::debug_span!(
1742            target: "lean_rs",
1743            "lean_rs.host.session.run_meta",
1744            service = service.name(),
1745            heartbeats = options.heartbeats(),
1746            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1747        )
1748        .entered();
1749        check_cancellation(cancellation)?;
1750        service.dispatch(self, request, options)
1751    }
1752
1753    /// Look up many declarations in one Lean traversal.
1754    ///
1755    /// Equivalent to calling [`Self::query_declaration`] in a loop over
1756    /// `names`, except that the entire batch crosses the FFI boundary
1757    /// exactly once: one `Array Name` allocation in, one
1758    /// `Array (Option Declaration)` allocation out. The Lean shim folds
1759    /// the singular `envQueryDeclaration` across the input array, so the
1760    /// iteration semantics are identical to a Rust-side fold over the
1761    /// singular path—a missing name still errors the batch.
1762    ///
1763    /// Names are still resolved through the capability's
1764    /// `name_from_string` shim, one [`lean_rs::LeanName`] handle per
1765    /// input. The metric impact is `names.len() + 1` recorded FFI calls
1766    /// for a batch of `names.len()` items, versus `2 * names.len()` for
1767    /// the same workload through [`Self::query_declaration`].
1768    ///
1769    /// # Errors
1770    ///
1771    /// Returns [`lean_rs::LeanError::Host`] with stage [`HostStage::Conversion`]
1772    /// on the first name that is not present in the imported
1773    /// environment, with the missing name in the diagnostic. Returns
1774    /// [`lean_rs::LeanError::LeanException`] if the Lean-side bulk shim raises
1775    /// through `IO`.
1776    pub fn query_declarations_bulk(
1777        &mut self,
1778        names: &[&str],
1779        cancellation: Option<&LeanCancellationToken>,
1780        progress: Option<&dyn LeanProgressSink>,
1781    ) -> LeanResult<Vec<LeanDeclaration<'lean>>> {
1782        let _span = tracing::debug_span!(
1783            target: "lean_rs",
1784            "lean_rs.host.session.query_declarations_bulk",
1785            batch_size = names.len(),
1786        )
1787        .entered();
1788        if names.is_empty() {
1789            return Ok(Vec::new());
1790        }
1791        check_cancellation(cancellation)?;
1792        if cancellation.is_some() {
1793            let started = Instant::now();
1794            let mut out = Vec::with_capacity(names.len());
1795            let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1796            for (idx, name) in names.iter().enumerate() {
1797                check_cancellation(cancellation)?;
1798                out.push(self.query_declaration(name, cancellation)?);
1799                report_progress(
1800                    progress,
1801                    "query_declarations_bulk",
1802                    u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
1803                    total,
1804                    started,
1805                )?;
1806            }
1807            return Ok(out);
1808        }
1809        let prepare_started = Instant::now();
1810        let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1811        let mut name_handles: Vec<LeanName<'lean>> = Vec::with_capacity(names.len());
1812        for (idx, name) in names.iter().enumerate() {
1813            name_handles.push(self.make_name(name, cancellation)?);
1814            report_progress(
1815                progress,
1816                "prepare_names",
1817                u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
1818                total,
1819                prepare_started,
1820            )?;
1821        }
1822        check_cancellation(cancellation)?;
1823        let raw = if let Some(sink) = progress {
1824            let bridge = ProgressBridge::new(sink, "query_declarations_bulk", total)?;
1825            let (handle, trampoline) = bridge.abi_parts();
1826            let t = Instant::now();
1827            let result = self.shims.env_query_declarations_bulk_progress.call(
1828                self.environment.clone(),
1829                name_handles,
1830                handle,
1831                trampoline,
1832            );
1833            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1834            self.record_call(batch_len, t.elapsed());
1835            bridge.decode::<Vec<Option<LeanDeclaration<'lean>>>>(result?)?
1836        } else {
1837            let t = Instant::now();
1838            let result = self
1839                .shims
1840                .env_query_declarations_bulk
1841                .call(self.environment.clone(), name_handles);
1842            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1843            self.record_call(batch_len, t.elapsed());
1844            result?
1845        };
1846        let mut out: Vec<LeanDeclaration<'lean>> = Vec::with_capacity(raw.len());
1847        for (slot, name) in raw.into_iter().zip(names.iter()) {
1848            match slot {
1849                Some(decl) => out.push(decl),
1850                None => {
1851                    return Err(lean_rs::abi::traits::conversion_error(format!(
1852                        "declaration '{name}' not found in imported environment"
1853                    )));
1854                }
1855            }
1856        }
1857        Ok(out)
1858    }
1859
1860    /// Parse and elaborate many independent Lean terms in one Lean
1861    /// traversal.
1862    ///
1863    /// Per-source `Result<LeanExpr, LeanElabFailure>` shape matches
1864    /// [`Self::elaborate`] exactly: outer [`LeanResult`] surfaces
1865    /// host-stack failures, inner per-source `Result` distinguishes
1866    /// successful elaboration from elaborator-reported diagnostics. A
1867    /// caller treating the bulk path as a fold over the singular path
1868    /// sees no semantic surprise.
1869    ///
1870    /// The `expected_type` parameter is **not** carried by the bulk
1871    /// shape: per-source expectations would force a parallel
1872    /// `&[Option<&LeanExpr>]` array, and no in-tree caller has earned
1873    /// the surface. Use [`Self::elaborate`] for individual terms with
1874    /// expected types.
1875    ///
1876    /// The heartbeat and diagnostic-byte budgets in `options` apply
1877    /// once each per source (the Lean shim builds fresh
1878    /// [`Lean.Options`](https://leanprover.github.io/) per item via the
1879    /// same `hostElaborate` path), so the per-batch upper bound on
1880    /// elapsed CPU work is `sources.len() * options.heartbeats()`.
1881    ///
1882    /// # Errors
1883    ///
1884    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side bulk shim
1885    /// raises through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
1886    /// [`HostStage::Conversion`] if the Lean return value does not
1887    /// decode into a `Vec<Result<LeanExpr, LeanElabFailure>>`.
1888    pub fn elaborate_bulk(
1889        &mut self,
1890        sources: &[&str],
1891        options: &LeanElabOptions,
1892        cancellation: Option<&LeanCancellationToken>,
1893        progress: Option<&dyn LeanProgressSink>,
1894    ) -> LeanResult<Vec<Result<LeanExpr<'lean>, LeanElabFailure>>> {
1895        let _span = tracing::debug_span!(
1896            target: "lean_rs",
1897            "lean_rs.host.session.elaborate_bulk",
1898            batch_size = sources.len(),
1899            heartbeats = options.heartbeats(),
1900            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1901        )
1902        .entered();
1903        if sources.is_empty() {
1904            return Ok(Vec::new());
1905        }
1906        check_cancellation(cancellation)?;
1907        if cancellation.is_some() {
1908            let started = Instant::now();
1909            let total = Some(u64::try_from(sources.len()).unwrap_or(u64::MAX));
1910            let mut out = Vec::with_capacity(sources.len());
1911            for (idx, source) in sources.iter().enumerate() {
1912                check_cancellation(cancellation)?;
1913                out.push(self.elaborate(source, None, options, cancellation)?);
1914                report_progress(
1915                    progress,
1916                    "elaborate_bulk",
1917                    u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
1918                    total,
1919                    started,
1920                )?;
1921            }
1922            return Ok(out);
1923        }
1924        let sources_owned: Vec<String> = sources.iter().map(|&s| s.to_owned()).collect();
1925        if let Some(sink) = progress {
1926            let total = Some(u64::try_from(sources.len()).unwrap_or(u64::MAX));
1927            let bridge = ProgressBridge::new(sink, "elaborate_bulk", total)?;
1928            let (handle, trampoline) = bridge.abi_parts();
1929            let t = Instant::now();
1930            let result = self.shims.elaborate_bulk_progress.call(
1931                self.environment.clone(),
1932                sources_owned,
1933                options.namespace_context_str().to_owned(),
1934                options.file_label_str().to_owned(),
1935                options.heartbeats(),
1936                options.diagnostic_byte_limit_usize(),
1937                handle,
1938                trampoline,
1939            );
1940            let batch_len = u64::try_from(sources.len()).unwrap_or(u64::MAX);
1941            self.record_call(batch_len, t.elapsed());
1942            bridge.decode(result?)
1943        } else {
1944            let t = Instant::now();
1945            let result = self.shims.elaborate_bulk.call(
1946                self.environment.clone(),
1947                sources_owned,
1948                options.namespace_context_str().to_owned(),
1949                options.file_label_str().to_owned(),
1950                options.heartbeats(),
1951                options.diagnostic_byte_limit_usize(),
1952            );
1953            let batch_len = u64::try_from(sources.len()).unwrap_or(u64::MAX);
1954            self.record_call(batch_len, t.elapsed());
1955            result
1956        }
1957    }
1958
1959    /// Build a `LeanName` from a dotted Rust string via the capability's
1960    /// `Name.toName` shim.
1961    fn make_name(&self, name: &str, cancellation: Option<&LeanCancellationToken>) -> LeanResult<LeanName<'lean>> {
1962        check_cancellation(cancellation)?;
1963        let lean_name = lean_rs::__host_internals::string_from_str(self.capabilities.host().runtime(), name);
1964        let t = Instant::now();
1965        let result = self.shims.name_from_string.call(lean_name);
1966        self.record_call(0, t.elapsed());
1967        result
1968    }
1969}
1970
1971trait HostMetaDispatch<'lean, Req, Resp> {
1972    fn dispatch(
1973        &self,
1974        session: &mut LeanSession<'lean, '_>,
1975        request: Req,
1976        options: &LeanMetaOptions,
1977    ) -> LeanResult<LeanMetaResponse<Resp>>;
1978}
1979
1980impl<'lean> HostMetaDispatch<'lean, LeanExpr<'lean>, LeanExpr<'lean>>
1981    for LeanMetaService<LeanExpr<'lean>, LeanExpr<'lean>>
1982{
1983    fn dispatch(
1984        &self,
1985        session: &mut LeanSession<'lean, '_>,
1986        request: LeanExpr<'lean>,
1987        options: &LeanMetaOptions,
1988    ) -> LeanResult<LeanMetaResponse<LeanExpr<'lean>>> {
1989        let Some(call) = (match self.name() {
1990            "lean_rs_host_meta_infer_type" => session.shims.meta_infer_type.as_ref(),
1991            "lean_rs_host_meta_whnf" => session.shims.meta_whnf.as_ref(),
1992            "lean_rs_host_meta_heartbeat_burn" => session.shims.meta_heartbeat_burn.as_ref(),
1993            _ => None,
1994        }) else {
1995            return Ok(unsupported_meta_response(self.name()));
1996        };
1997        let t = Instant::now();
1998        let result = call.call(
1999            session.environment.clone(),
2000            request,
2001            options.heartbeats(),
2002            options.diagnostic_byte_limit_usize(),
2003            options.transparency_byte(),
2004        );
2005        session.record_call(0, t.elapsed());
2006        result
2007    }
2008}
2009
2010impl<'lean>
2011    HostMetaDispatch<
2012        'lean,
2013        (
2014            LeanExpr<'lean>,
2015            LeanExpr<'lean>,
2016            crate::host::meta::LeanMetaTransparency,
2017        ),
2018        bool,
2019    >
2020    for LeanMetaService<
2021        (
2022            LeanExpr<'lean>,
2023            LeanExpr<'lean>,
2024            crate::host::meta::LeanMetaTransparency,
2025        ),
2026        bool,
2027    >
2028{
2029    fn dispatch(
2030        &self,
2031        session: &mut LeanSession<'lean, '_>,
2032        request: (
2033            LeanExpr<'lean>,
2034            LeanExpr<'lean>,
2035            crate::host::meta::LeanMetaTransparency,
2036        ),
2037        options: &LeanMetaOptions,
2038    ) -> LeanResult<LeanMetaResponse<bool>> {
2039        let Some(call) = session.shims.meta_is_def_eq.as_ref() else {
2040            return Ok(unsupported_meta_response(self.name()));
2041        };
2042        let t = Instant::now();
2043        let result = call.call(
2044            session.environment.clone(),
2045            request,
2046            options.heartbeats(),
2047            options.diagnostic_byte_limit_usize(),
2048            options.transparency_byte(),
2049        );
2050        session.record_call(0, t.elapsed());
2051        result
2052    }
2053}
2054
2055impl<'lean> HostMetaDispatch<'lean, LeanExpr<'lean>, String> for LeanMetaService<LeanExpr<'lean>, String> {
2056    fn dispatch(
2057        &self,
2058        session: &mut LeanSession<'lean, '_>,
2059        request: LeanExpr<'lean>,
2060        options: &LeanMetaOptions,
2061    ) -> LeanResult<LeanMetaResponse<String>> {
2062        let Some(call) = session.shims.meta_pp_expr.as_ref() else {
2063            return Ok(unsupported_meta_response(self.name()));
2064        };
2065        let t = Instant::now();
2066        let result = call.call(
2067            session.environment.clone(),
2068            request,
2069            options.heartbeats(),
2070            options.diagnostic_byte_limit_usize(),
2071            options.transparency_byte(),
2072        );
2073        session.record_call(0, t.elapsed());
2074        result
2075    }
2076}
2077
2078fn unsupported_meta_response<Resp>(symbol: &str) -> LeanMetaResponse<Resp> {
2079    LeanMetaResponse::Unsupported(LeanElabFailure::synthetic(
2080        format!("bundled host shim does not export meta service '{symbol}'"),
2081        "<lean-rs-host meta>".to_owned(),
2082    ))
2083}