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