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