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