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    /// Value of the process-global extension-registration stamp at the moment
560    /// this session's environment was imported. See
561    /// [`Self::extension_registry_epoch`] for what a mismatch means.
562    extension_registry_epoch: u64,
563    /// Per-session dispatch metrics. `Cell` because every query method
564    /// takes `&mut self` but the bulk path can also be invoked through a
565    /// shared reference (e.g. inside a fold helper)—keeping the
566    /// counter in `Cell` makes the recording uniform without adding an
567    /// extra `&mut` borrow at each call site.
568    stats: Cell<SessionStats>,
569}
570
571/// Process-wide serialization for [`LeanSession::import`]. See the
572/// comment at the lock-acquire site for the Lean-4.30 race it closes.
573static SESSION_IMPORT_LOCK: Mutex<()> = Mutex::new(());
574
575pub(crate) fn with_session_import_lock<T>(f: impl FnOnce() -> LeanResult<T>) -> LeanResult<T> {
576    let _import_guard = SESSION_IMPORT_LOCK
577        .lock()
578        .unwrap_or_else(|poisoned| poisoned.into_inner());
579    f()
580}
581
582impl<'lean, 'c> LeanSession<'lean, 'c> {
583    pub(crate) fn import_search_paths(capabilities: &LeanCapabilities<'lean, '_>) -> LeanResult<Vec<String>> {
584        let project = capabilities.host().project();
585        let mut search_paths: Vec<String> = project
586            .olean_search_paths()
587            .into_iter()
588            .map(|path| path.to_string_lossy().into_owned())
589            .collect();
590        search_paths.push(
591            crate::host::lake::LakeProject::interop_olean_search_path()?
592                .to_string_lossy()
593                .into_owned(),
594        );
595        search_paths.push(
596            crate::host::lake::LakeProject::shim_olean_search_path()?
597                .to_string_lossy()
598                .into_owned(),
599        );
600        Ok(search_paths)
601    }
602
603    /// Import the named modules into a fresh Lean environment and wrap
604    /// it as a session.
605    ///
606    /// The Lean-side `lean_rs_host_session_import` receives the Lake
607    /// project root (so it can `Lean.initSearchPath` the `.olean`
608    /// directory) and the module-name list, and returns the resulting
609    /// environment. Failures surface as
610    /// [`lean_rs::LeanError::LeanException`] with the message Lean produced.
611    pub(crate) fn import(
612        capabilities: &'c LeanCapabilities<'lean, 'c>,
613        imports: &[&str],
614        cancellation: Option<&LeanCancellationToken>,
615        progress: Option<&dyn LeanProgressSink>,
616    ) -> LeanResult<Self> {
617        Self::import_with_profile(
618            capabilities,
619            imports,
620            LeanSessionImportProfile::default(),
621            cancellation,
622            progress,
623        )
624    }
625
626    pub(crate) fn import_with_profile(
627        capabilities: &'c LeanCapabilities<'lean, 'c>,
628        imports: &[&str],
629        profile: LeanSessionImportProfile,
630        cancellation: Option<&LeanCancellationToken>,
631        progress: Option<&dyn LeanProgressSink>,
632    ) -> LeanResult<Self> {
633        let _span = tracing::info_span!(
634            target: "lean_rs",
635            "lean_rs.host.session.import",
636            profile = profile.label(),
637            imports_len = imports.len(),
638        )
639        .entered();
640        check_cancellation(cancellation)?;
641        let search_paths = Self::import_search_paths(capabilities)?;
642        let imports_owned: Vec<String> = imports.iter().map(|&s| s.to_owned()).collect();
643        // Lean 4.30 strictly enforces `enableInitializersExecution` before
644        // `importModules (loadExts := true)`. The flag is process-global,
645        // but `Lean.withImporting` (wrapped around every import) resets it
646        // on completion—two threads importing concurrently race the
647        // shim's enable→import sequence and the loser sees the flag
648        // cleared by the winner's reset. Serializing the import phase
649        // across the process matches Lean's "single execution thread
650        // accessing the global references" requirement. Sessions operate
651        // concurrently on their own `Environment` values once import
652        // returns; the lock spans only the FFI call.
653        with_session_import_lock(|| {
654            let shims = HostShimBindings::resolve(capabilities.shim_capability())
655                .map_err(|err| binding_error_to_lean_error(&err))?;
656            let environment = if let Some(sink) = progress {
657                let bridge =
658                    ProgressBridge::new(sink, "import", Some(u64::try_from(imports.len()).unwrap_or(u64::MAX)))?;
659                let (handle, trampoline) = bridge.abi_parts();
660                let raw = if profile == LeanSessionImportProfile::default() {
661                    shims
662                        .session_import_progress
663                        .call(search_paths, imports_owned, handle, trampoline)?
664                } else {
665                    shims.session_import_profile_progress.call(
666                        search_paths,
667                        imports_owned,
668                        profile.import_all(),
669                        profile.import_level().code(),
670                        handle,
671                        trampoline,
672                    )?
673                };
674                bridge.decode(raw)?
675            } else if profile == LeanSessionImportProfile::default() {
676                shims.session_import.call(search_paths, imports_owned)?
677            } else {
678                shims.session_import_profile.call(
679                    search_paths,
680                    imports_owned,
681                    profile.import_all(),
682                    profile.import_level().code(),
683                    profile.load_exts(),
684                    false,
685                    false,
686                    String::new(),
687                )?
688            };
689            let import_stats = shims.env_import_stats.call(
690                environment.clone(),
691                profile.import_level().as_str().to_owned(),
692                profile.load_exts(),
693            )?;
694            // Read after the import, and inside the import lock, so the stamp
695            // describes the registry as it stood once this environment's own
696            // initializers had run. Reading it earlier would record a value
697            // this environment is already newer than.
698            let extension_registry_epoch = shims.extension_registry_epoch.call()?;
699            Ok(Self {
700                capabilities,
701                shims,
702                environment,
703                import_stats,
704                extension_registry_epoch,
705                stats: Cell::new(SessionStats::default()),
706            })
707        })
708    }
709
710    pub(crate) fn import_profiled(
711        capabilities: &'c LeanCapabilities<'lean, 'c>,
712        imports: &[&str],
713        mode: LeanImportProfileMode,
714        profiler_options: &LeanImportProfilerOptions,
715    ) -> LeanResult<Self> {
716        let _span = tracing::info_span!(
717            target: "lean_rs",
718            "lean_rs.host.session.import_profiled",
719            mode = mode.label(),
720            imports_len = imports.len(),
721        )
722        .entered();
723        let search_paths = Self::import_search_paths(capabilities)?;
724        let imports_owned: Vec<String> = imports.iter().map(|&s| s.to_owned()).collect();
725        with_session_import_lock(|| {
726            let shims = HostShimBindings::resolve(capabilities.shim_capability())
727                .map_err(|err| binding_error_to_lean_error(&err))?;
728            let environment = shims.session_import_profile.call(
729                search_paths,
730                imports_owned,
731                mode.import_all(),
732                mode.import_level().code(),
733                mode.load_exts(),
734                profiler_options.profiler,
735                profiler_options.trace_profiler,
736                profiler_options.trace_profiler_output.clone().unwrap_or_default(),
737            )?;
738            let import_stats = shims.env_import_stats.call(
739                environment.clone(),
740                mode.import_level().as_str().to_owned(),
741                mode.load_exts(),
742            )?;
743            let extension_registry_epoch = shims.extension_registry_epoch.call()?;
744            Ok(Self {
745                capabilities,
746                shims,
747                environment,
748                import_stats,
749                extension_registry_epoch,
750                stats: Cell::new(SessionStats::default()),
751            })
752        })
753    }
754
755    /// Wrap a previously-imported `Lean.Environment` as a fresh
756    /// [`LeanSession`] over `capabilities`.
757    ///
758    /// Crate-private; only [`crate::host::pool::SessionPool::acquire`]
759    /// calls this to recycle a pooled environment under a new
760    /// capability borrow. The returned session's [`SessionStats`] start
761    /// at zero—accumulated counters from the previous owner do not
762    /// leak across pool checkouts.
763    pub(crate) fn from_environment_with_import_stats(
764        capabilities: &'c LeanCapabilities<'lean, 'c>,
765        environment: Obj<'lean>,
766        import_stats: LeanImportStats,
767        extension_registry_epoch: u64,
768    ) -> LeanResult<Self> {
769        let shims = HostShimBindings::resolve(capabilities.shim_capability())
770            .map_err(|err| binding_error_to_lean_error(&err))?;
771        Ok(Self {
772            capabilities,
773            shims,
774            environment,
775            import_stats,
776            extension_registry_epoch,
777            stats: Cell::new(SessionStats::default()),
778        })
779    }
780
781    /// Consume the session and return its owned `Lean.Environment`.
782    ///
783    /// Crate-private; only [`crate::host::pool::SessionPool`] uses this
784    /// to reclaim the environment when a [`crate::host::pool::PooledSession`]
785    /// drops. The returned `Obj<'lean>` carries one Lean refcount, which
786    /// the pool is responsible for either pushing back into the free
787    /// list (in which case `Drop` runs later when the pool itself
788    /// drops) or releasing immediately (when at capacity).
789    pub(crate) fn into_environment(self) -> Obj<'lean> {
790        self.environment
791    }
792
793    /// Snapshot of this session's accumulated dispatch metrics.
794    ///
795    /// Returns a copy; the counters keep accumulating after the call.
796    /// Use [`SessionStats::default`] to compute a delta across two
797    /// snapshots.
798    #[must_use]
799    pub fn stats(&self) -> SessionStats {
800        self.stats.get()
801    }
802
803    /// Lean-native attribution for this session's imported environment.
804    #[must_use]
805    pub fn import_stats(&self) -> &LeanImportStats {
806        &self.import_stats
807    }
808
809    /// The process-global extension-registration stamp as it stood when this
810    /// session's environment was imported.
811    ///
812    /// Opaque and monotone; only equality against
813    /// [`Self::live_extension_registry_epoch`] carries meaning — never compare
814    /// two stamps with `<`. When the two differ, a later import registered a
815    /// Lean environment extension and this environment's `extensions` array is
816    /// permanently shorter than the registry: Lean sizes it once, at
817    /// `finalizeImport`, and keeps both the growth helper and the field
818    /// `private`. Elaborating any `namespace`, `section`, or `open … in`
819    /// against it makes `ScopedEnvExtension.pushScope` index past the end and
820    /// `panic!`. There is no repair — the only sound response is to drop the
821    /// session and re-import.
822    ///
823    /// A bare non-persistent `registerEnvExtension` moves none of the three
824    /// registries the stamp sums, so in principle it can grow the array
825    /// requirement without moving the stamp. The resulting mismatch is not
826    /// reachable: extension indices are shared across kinds, so if such an
827    /// extension lands at index *n* then `scopedEnvExtensionsRef` did not grow
828    /// and every scoped extension the blind iteration touches still has an
829    /// index inside the short array. The only other blind walks are over
830    /// `persistentEnvExtensionsRef` — import finalization, and olean writing,
831    /// which this host never performs. Beyond those, such an extension is
832    /// reached only through an explicit `ext.getState env` from the module that
833    /// registered it, and that module is not in the stale environment's import
834    /// closure. Re-check this argument at every toolchain bump.
835    #[must_use]
836    pub fn extension_registry_epoch(&self) -> u64 {
837        self.extension_registry_epoch
838    }
839
840    /// The current process-global extension-registration stamp.
841    ///
842    /// Compare against [`Self::extension_registry_epoch`] to decide whether
843    /// this session's environment is still safe to elaborate against. The
844    /// stamp is process-global, so one read answers for every live session,
845    /// whichever one it is read through.
846    ///
847    /// # Errors
848    ///
849    /// Returns an error if the shim raises through `IO` or the result cannot
850    /// be decoded.
851    pub fn live_extension_registry_epoch(&self) -> LeanResult<u64> {
852        let started = Instant::now();
853        let epoch = self.shims.extension_registry_epoch.call();
854        self.record_call(0, started.elapsed());
855        epoch
856    }
857
858    /// Internal helper: record one FFI call and add `batch` per-item
859    /// entries plus `elapsed` nanoseconds. Singular methods pass
860    /// `batch = 0`; bulk methods pass the input length.
861    fn record_call(&self, batch: u64, elapsed: std::time::Duration) {
862        let mut s = self.stats.get();
863        s.ffi_calls = s.ffi_calls.saturating_add(1);
864        s.batch_items = s.batch_items.saturating_add(batch);
865        s.elapsed_ns = s
866            .elapsed_ns
867            .saturating_add(u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX));
868        self.stats.set(s);
869    }
870
871    fn decode_strings_cached(raw: Vec<Obj<'lean>>) -> LeanResult<Vec<String>> {
872        if raw.is_empty() {
873            return Ok(Vec::new());
874        }
875        let Some(first_key) = raw.first().map(Obj::as_raw_borrowed) else {
876            return Ok(Vec::new());
877        };
878        if raw.iter().all(|obj| obj.as_raw_borrowed() == first_key) {
879            let len = raw.len();
880            let mut raw_iter = raw.into_iter();
881            let Some(first) = raw_iter.next() else {
882                return Ok(Vec::new());
883            };
884            let value = String::try_from_lean(first)?;
885            return Ok(vec![value; len]);
886        }
887        let mut out = Vec::with_capacity(raw.len());
888        for obj in raw {
889            out.push(String::try_from_lean(obj)?);
890        }
891        Ok(out)
892    }
893
894    fn all_equal_name<'a>(names: &'a [&str]) -> Option<&'a str> {
895        let first = *names.first()?;
896        names.iter().all(|name| *name == first).then_some(first)
897    }
898
899    /// Look up a declaration by full Lean name (e.g. `"Nat.zero"`).
900    ///
901    /// # Errors
902    ///
903    /// Returns [`lean_rs::LeanError::Host`] with stage [`HostStage::Conversion`]
904    /// if the name is not present in the imported environment. Returns
905    /// [`lean_rs::LeanError::LeanException`] if the Lean-side query raises.
906    pub fn query_declaration(
907        &mut self,
908        name: &str,
909        cancellation: Option<&LeanCancellationToken>,
910    ) -> LeanResult<LeanDeclaration<'lean>> {
911        let _span = tracing::debug_span!(
912            target: "lean_rs",
913            "lean_rs.host.session.query_declaration",
914            name = name,
915        )
916        .entered();
917        check_cancellation(cancellation)?;
918        let name_handle = self.make_name(name, cancellation)?;
919        check_cancellation(cancellation)?;
920        let t = Instant::now();
921        let result = self
922            .shims
923            .env_query_declaration
924            .call(self.environment.clone(), name_handle);
925        self.record_call(0, t.elapsed());
926        match result? {
927            Some(decl) => Ok(decl),
928            None => Err(lean_rs::abi::traits::conversion_error(format!(
929                "declaration '{name}' not found in imported environment"
930            ))),
931        }
932    }
933
934    /// All declaration names in the imported environment.
935    ///
936    /// Returns a Vec; the environment's `constants` map contains many
937    /// thousands of entries even for a small project (Lean prelude is
938    /// always imported), so prefer [`LeanSession::query_declaration`]
939    /// when you already know the name.
940    ///
941    /// # Errors
942    ///
943    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side query
944    /// raises.
945    pub fn list_declarations(
946        &mut self,
947        cancellation: Option<&LeanCancellationToken>,
948    ) -> LeanResult<Vec<LeanName<'lean>>> {
949        let _span = tracing::debug_span!(
950            target: "lean_rs",
951            "lean_rs.host.session.list_declarations",
952        )
953        .entered();
954        check_cancellation(cancellation)?;
955        let t = Instant::now();
956        let raw = self.shims.env_list_declarations.call(self.environment.clone());
957        self.record_call(0, t.elapsed());
958        raw?.into_iter().map(LeanName::try_from_lean).collect()
959    }
960
961    /// Declaration names matching `filter`.
962    ///
963    /// Filtering runs inside Lean while traversing the environment
964    /// constants table, so Rust only allocates handles for names the
965    /// caller asked to keep.
966    ///
967    /// # Errors
968    ///
969    /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
970    /// already cancelled before dispatch. Returns
971    /// [`lean_rs::LeanError::LeanException`] if the Lean-side query
972    /// raises.
973    pub fn list_declarations_filtered(
974        &mut self,
975        filter: &LeanDeclarationFilter,
976        cancellation: Option<&LeanCancellationToken>,
977        progress: Option<&dyn LeanProgressSink>,
978    ) -> LeanResult<Vec<LeanName<'lean>>> {
979        let _span = tracing::debug_span!(
980            target: "lean_rs",
981            "lean_rs.host.session.list_declarations_filtered",
982            include_private = filter.include_private,
983            include_generated = filter.include_generated,
984            include_internal = filter.include_internal,
985        )
986        .entered();
987        check_cancellation(cancellation)?;
988        let raw = if let Some(sink) = progress {
989            let bridge = ProgressBridge::new(sink, "list_declarations_filtered", None)?;
990            let (handle, trampoline) = bridge.abi_parts();
991            let t = Instant::now();
992            let result = self.shims.env_list_declarations_filtered_progress.call(
993                self.environment.clone(),
994                *filter,
995                handle,
996                trampoline,
997            );
998            self.record_call(0, t.elapsed());
999            bridge.decode::<Vec<Obj<'lean>>>(result?)?
1000        } else {
1001            let t = Instant::now();
1002            let result = self
1003                .shims
1004                .env_list_declarations_filtered
1005                .call(self.environment.clone(), *filter);
1006            self.record_call(0, t.elapsed());
1007            result?
1008        };
1009        raw.into_iter().map(LeanName::try_from_lean).collect()
1010    }
1011
1012    /// Source range Lean recorded for `name`.
1013    ///
1014    /// Returns `Ok(None)` when the name is absent or Lean has no
1015    /// declaration range for it. That is normal for synthetic,
1016    /// runtime-created, and some compiler-generated declarations.
1017    ///
1018    /// # Errors
1019    ///
1020    /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
1021    /// already cancelled before dispatch. Returns
1022    /// [`lean_rs::LeanError::LeanException`] if the Lean-side query
1023    /// raises.
1024    pub fn declaration_source_range(
1025        &mut self,
1026        name: &str,
1027        cancellation: Option<&LeanCancellationToken>,
1028    ) -> LeanResult<Option<LeanSourceRange>> {
1029        let _span = tracing::debug_span!(
1030            target: "lean_rs",
1031            "lean_rs.host.session.declaration_source_range",
1032            name = name,
1033        )
1034        .entered();
1035        check_cancellation(cancellation)?;
1036        let name_handle = self.make_name(name, cancellation)?;
1037        check_cancellation(cancellation)?;
1038        let source_roots = self
1039            .capabilities
1040            .host()
1041            .project()
1042            .source_roots()?
1043            .into_iter()
1044            .map(|path| path.to_string_lossy().into_owned())
1045            .collect::<Vec<_>>();
1046        check_cancellation(cancellation)?;
1047        let t = Instant::now();
1048        let result = self
1049            .shims
1050            .env_declaration_source_range
1051            .call(self.environment.clone(), name_handle, source_roots);
1052        self.record_call(0, t.elapsed());
1053        result
1054    }
1055
1056    /// The declared type of `name`, as an opaque [`LeanExpr`] handle.
1057    ///
1058    /// Returns `Ok(None)` if the name is not present in the environment.
1059    ///
1060    /// # Errors
1061    ///
1062    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side query
1063    /// raises.
1064    pub fn declaration_type(
1065        &mut self,
1066        name: &str,
1067        cancellation: Option<&LeanCancellationToken>,
1068    ) -> LeanResult<Option<LeanExpr<'lean>>> {
1069        let _span = tracing::debug_span!(
1070            target: "lean_rs",
1071            "lean_rs.host.session.declaration_type",
1072            name = name,
1073        )
1074        .entered();
1075        check_cancellation(cancellation)?;
1076        let name_handle = self.make_name(name, cancellation)?;
1077        check_cancellation(cancellation)?;
1078        let t = Instant::now();
1079        let result = self
1080            .shims
1081            .env_declaration_type
1082            .call(self.environment.clone(), name_handle);
1083        self.record_call(0, t.elapsed());
1084        result
1085    }
1086
1087    /// The declared types of `names`, preserving input order.
1088    ///
1089    /// Returns `None` in each slot whose name is not present in the
1090    /// environment. With `cancellation = None`, the whole batch crosses
1091    /// the FFI boundary once and Lean converts the input strings to
1092    /// names internally. With `Some(token)`, this loops through
1093    /// [`Self::declaration_type`] so cancellation can be observed
1094    /// between items; partial results are discarded when cancellation
1095    /// fires.
1096    ///
1097    /// # Errors
1098    ///
1099    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side
1100    /// bulk shim raises through `IO`.
1101    pub fn declaration_type_bulk(
1102        &mut self,
1103        names: &[&str],
1104        cancellation: Option<&LeanCancellationToken>,
1105        progress: Option<&dyn LeanProgressSink>,
1106    ) -> LeanResult<Vec<Option<LeanExpr<'lean>>>> {
1107        let _span = tracing::debug_span!(
1108            target: "lean_rs",
1109            "lean_rs.host.session.declaration_type_bulk",
1110            batch_size = names.len(),
1111        )
1112        .entered();
1113        if names.is_empty() {
1114            return Ok(Vec::new());
1115        }
1116        check_cancellation(cancellation)?;
1117        if cancellation.is_some() {
1118            let started = Instant::now();
1119            let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1120            let mut out = Vec::with_capacity(names.len());
1121            for (idx, name) in names.iter().enumerate() {
1122                check_cancellation(cancellation)?;
1123                out.push(self.declaration_type(name, cancellation)?);
1124                report_progress(
1125                    progress,
1126                    "declaration_type_bulk",
1127                    u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
1128                    total,
1129                    started,
1130                )?;
1131            }
1132            return Ok(out);
1133        }
1134        if progress.is_none()
1135            && let Some(name) = Self::all_equal_name(names)
1136        {
1137            let names_owned = vec![name.to_owned()];
1138            let t = Instant::now();
1139            let mut result = self
1140                .shims
1141                .env_declaration_type_bulk
1142                .call(self.environment.clone(), names_owned)?;
1143            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1144            self.record_call(batch_len, t.elapsed());
1145            let value = result.pop().unwrap_or(None);
1146            return Ok(vec![value; names.len()]);
1147        }
1148        let names_owned: Vec<String> = names.iter().map(|&name| name.to_owned()).collect();
1149        if let Some(sink) = progress {
1150            let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1151            let bridge = ProgressBridge::new(sink, "declaration_type_bulk", total)?;
1152            let (handle, trampoline) = bridge.abi_parts();
1153            let t = Instant::now();
1154            let result = self.shims.env_declaration_type_bulk_progress.call(
1155                self.environment.clone(),
1156                names_owned,
1157                handle,
1158                trampoline,
1159            );
1160            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1161            self.record_call(batch_len, t.elapsed());
1162            bridge.decode(result?)
1163        } else {
1164            let t = Instant::now();
1165            let result = self
1166                .shims
1167                .env_declaration_type_bulk
1168                .call(self.environment.clone(), names_owned);
1169            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1170            self.record_call(batch_len, t.elapsed());
1171            result
1172        }
1173    }
1174
1175    /// The kind of `name` as a Lean-rendered string
1176    /// (`"axiom"`, `"definition"`, `"theorem"`, `"opaque"`, `"quot"`,
1177    /// `"inductive"`, `"constructor"`, `"recursor"`), or `"missing"`
1178    /// if `name` is not in the environment.
1179    ///
1180    /// # Errors
1181    ///
1182    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side query
1183    /// raises.
1184    pub fn declaration_kind(&mut self, name: &str, cancellation: Option<&LeanCancellationToken>) -> LeanResult<String> {
1185        let _span = tracing::debug_span!(
1186            target: "lean_rs",
1187            "lean_rs.host.session.declaration_kind",
1188            name = name,
1189        )
1190        .entered();
1191        check_cancellation(cancellation)?;
1192        let name_handle = self.make_name(name, cancellation)?;
1193        check_cancellation(cancellation)?;
1194        let t = Instant::now();
1195        let result = self
1196            .shims
1197            .env_declaration_kind
1198            .call(self.environment.clone(), name_handle);
1199        self.record_call(0, t.elapsed());
1200        result
1201    }
1202
1203    /// The declaration kinds of `names`, preserving input order.
1204    ///
1205    /// Each output slot is the same string that [`Self::declaration_kind`]
1206    /// would return for the corresponding input, including `"missing"`
1207    /// for absent declarations. With `cancellation = None`, this is one
1208    /// Lean-side bulk dispatch over an `Array String`; with
1209    /// `Some(token)`, this loops through the singular path so the token
1210    /// can be checked between items.
1211    ///
1212    /// # Errors
1213    ///
1214    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side
1215    /// bulk shim raises through `IO`.
1216    pub fn declaration_kind_bulk(
1217        &mut self,
1218        names: &[&str],
1219        cancellation: Option<&LeanCancellationToken>,
1220        progress: Option<&dyn LeanProgressSink>,
1221    ) -> LeanResult<Vec<String>> {
1222        let _span = tracing::debug_span!(
1223            target: "lean_rs",
1224            "lean_rs.host.session.declaration_kind_bulk",
1225            batch_size = names.len(),
1226        )
1227        .entered();
1228        if names.is_empty() {
1229            return Ok(Vec::new());
1230        }
1231        check_cancellation(cancellation)?;
1232        if cancellation.is_some() {
1233            let started = Instant::now();
1234            let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1235            let mut out = Vec::with_capacity(names.len());
1236            for (idx, name) in names.iter().enumerate() {
1237                check_cancellation(cancellation)?;
1238                out.push(self.declaration_kind(name, cancellation)?);
1239                report_progress(
1240                    progress,
1241                    "declaration_kind_bulk",
1242                    u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
1243                    total,
1244                    started,
1245                )?;
1246            }
1247            return Ok(out);
1248        }
1249        if progress.is_none()
1250            && let Some(name) = Self::all_equal_name(names)
1251        {
1252            let names_owned = vec![name.to_owned()];
1253            let t = Instant::now();
1254            let mut result = Self::decode_strings_cached(
1255                self.shims
1256                    .env_declaration_kind_bulk
1257                    .call(self.environment.clone(), names_owned)?,
1258            )?;
1259            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1260            self.record_call(batch_len, t.elapsed());
1261            let value = result.pop().unwrap_or_default();
1262            return Ok(vec![value; names.len()]);
1263        }
1264        let names_owned: Vec<String> = names.iter().map(|&name| name.to_owned()).collect();
1265        if let Some(sink) = progress {
1266            let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1267            let bridge = ProgressBridge::new(sink, "declaration_kind_bulk", total)?;
1268            let (handle, trampoline) = bridge.abi_parts();
1269            let t = Instant::now();
1270            let result = self.shims.env_declaration_kind_bulk_progress.call(
1271                self.environment.clone(),
1272                names_owned,
1273                handle,
1274                trampoline,
1275            );
1276            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1277            self.record_call(batch_len, t.elapsed());
1278            let raw = bridge.decode::<Vec<Obj<'lean>>>(result?)?;
1279            Self::decode_strings_cached(raw)
1280        } else {
1281            let t = Instant::now();
1282            let result = self
1283                .shims
1284                .env_declaration_kind_bulk
1285                .call(self.environment.clone(), names_owned);
1286            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1287            self.record_call(batch_len, t.elapsed());
1288            Self::decode_strings_cached(result?)
1289        }
1290    }
1291
1292    /// The Lean-rendered display string of `name`. Round-trips a name
1293    /// through the capability's `Name.toString` shim so callers see the
1294    /// same canonical form Lean would log.
1295    ///
1296    /// Diagnostic only—not a semantic key. Use
1297    /// [`LeanSession::query_declaration`] + a typed handle when
1298    /// equality matters.
1299    ///
1300    /// # Errors
1301    ///
1302    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side query
1303    /// raises.
1304    pub fn declaration_name(&mut self, name: &str, cancellation: Option<&LeanCancellationToken>) -> LeanResult<String> {
1305        let _span = tracing::debug_span!(
1306            target: "lean_rs",
1307            "lean_rs.host.session.declaration_name",
1308            name = name,
1309        )
1310        .entered();
1311        check_cancellation(cancellation)?;
1312        let name_handle = self.make_name(name, cancellation)?;
1313        check_cancellation(cancellation)?;
1314        let t = Instant::now();
1315        let result = self
1316            .shims
1317            .env_declaration_name
1318            .call(self.environment.clone(), name_handle);
1319        self.record_call(0, t.elapsed());
1320        result
1321    }
1322
1323    /// Lean-rendered display strings for `names`, preserving input
1324    /// order.
1325    ///
1326    /// This is diagnostic text, not a semantic key. Missing
1327    /// declarations are not an error because the singular
1328    /// [`Self::declaration_name`] path also only round-trips the input
1329    /// name through Lean's `Name.toString` renderer.
1330    ///
1331    /// With `cancellation = None`, this is one Lean-side bulk dispatch
1332    /// over an `Array String`; with `Some(token)`, this loops through
1333    /// the singular path so the token can be checked between items.
1334    ///
1335    /// # Errors
1336    ///
1337    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side
1338    /// bulk shim raises through `IO`.
1339    pub fn declaration_name_bulk(
1340        &mut self,
1341        names: &[&str],
1342        cancellation: Option<&LeanCancellationToken>,
1343        progress: Option<&dyn LeanProgressSink>,
1344    ) -> LeanResult<Vec<String>> {
1345        let _span = tracing::debug_span!(
1346            target: "lean_rs",
1347            "lean_rs.host.session.declaration_name_bulk",
1348            batch_size = names.len(),
1349        )
1350        .entered();
1351        if names.is_empty() {
1352            return Ok(Vec::new());
1353        }
1354        check_cancellation(cancellation)?;
1355        if cancellation.is_some() {
1356            let started = Instant::now();
1357            let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1358            let mut out = Vec::with_capacity(names.len());
1359            for (idx, name) in names.iter().enumerate() {
1360                check_cancellation(cancellation)?;
1361                out.push(self.declaration_name(name, cancellation)?);
1362                report_progress(
1363                    progress,
1364                    "declaration_name_bulk",
1365                    u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
1366                    total,
1367                    started,
1368                )?;
1369            }
1370            return Ok(out);
1371        }
1372        if progress.is_none()
1373            && let Some(name) = Self::all_equal_name(names)
1374        {
1375            let names_owned = vec![name.to_owned()];
1376            let t = Instant::now();
1377            let mut result = Self::decode_strings_cached(
1378                self.shims
1379                    .env_declaration_name_bulk
1380                    .call(self.environment.clone(), names_owned)?,
1381            )?;
1382            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1383            self.record_call(batch_len, t.elapsed());
1384            let value = result.pop().unwrap_or_default();
1385            return Ok(vec![value; names.len()]);
1386        }
1387        let names_owned: Vec<String> = names.iter().map(|&name| name.to_owned()).collect();
1388        if let Some(sink) = progress {
1389            let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1390            let bridge = ProgressBridge::new(sink, "declaration_name_bulk", total)?;
1391            let (handle, trampoline) = bridge.abi_parts();
1392            let t = Instant::now();
1393            let result = self.shims.env_declaration_name_bulk_progress.call(
1394                self.environment.clone(),
1395                names_owned,
1396                handle,
1397                trampoline,
1398            );
1399            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1400            self.record_call(batch_len, t.elapsed());
1401            let raw = bridge.decode::<Vec<Obj<'lean>>>(result?)?;
1402            Self::decode_strings_cached(raw)
1403        } else {
1404            let t = Instant::now();
1405            let result = self
1406                .shims
1407                .env_declaration_name_bulk
1408                .call(self.environment.clone(), names_owned);
1409            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1410            self.record_call(batch_len, t.elapsed());
1411            Self::decode_strings_cached(result?)
1412        }
1413    }
1414
1415    /// Render an opaque [`LeanName`] handle as its dotted-string form,
1416    /// routed through the capability's `Name.toString` shim.
1417    ///
1418    /// This is the supported way to turn a `LeanName` (e.g. an element
1419    /// of [`Self::list_declarations_filtered`]'s result) into Rust text.
1420    /// The output is diagnostic—not a semantic key—and equality on
1421    /// the underlying `Lean.Name` still lives in Lean.
1422    ///
1423    /// # Errors
1424    ///
1425    /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
1426    /// already cancelled before dispatch.
1427    pub fn name_to_string(
1428        &mut self,
1429        name: &LeanName<'lean>,
1430        cancellation: Option<&LeanCancellationToken>,
1431    ) -> LeanResult<String> {
1432        let _span = tracing::debug_span!(target: "lean_rs", "lean_rs.host.session.name_to_string").entered();
1433        check_cancellation(cancellation)?;
1434        let t = Instant::now();
1435        let result = self.shims.name_to_string.call(name.clone());
1436        self.record_call(0, t.elapsed());
1437        result
1438    }
1439
1440    /// Render `names` as dotted-string forms, preserving input order.
1441    ///
1442    /// Implemented as a per-item loop over [`Self::name_to_string`] in
1443    /// v1: cancellation is checked between items, progress is reported
1444    /// after each. The Lean shim is pure and short, so the per-item FFI
1445    /// overhead is acceptable; a bulk shim is a future optimisation if
1446    /// profiling shows it matters.
1447    ///
1448    /// # Errors
1449    ///
1450    /// Returns [`lean_rs::LeanError::Cancelled`] between items if the
1451    /// token is tripped during the walk.
1452    pub fn name_to_string_bulk(
1453        &mut self,
1454        names: &[LeanName<'lean>],
1455        cancellation: Option<&LeanCancellationToken>,
1456        progress: Option<&dyn LeanProgressSink>,
1457    ) -> LeanResult<Vec<String>> {
1458        let _span = tracing::debug_span!(
1459            target: "lean_rs",
1460            "lean_rs.host.session.name_to_string_bulk",
1461            batch_size = names.len(),
1462        )
1463        .entered();
1464        if names.is_empty() {
1465            return Ok(Vec::new());
1466        }
1467        check_cancellation(cancellation)?;
1468        let started = Instant::now();
1469        let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1470        let mut out = Vec::with_capacity(names.len());
1471        for (idx, name) in names.iter().enumerate() {
1472            check_cancellation(cancellation)?;
1473            out.push(self.name_to_string(name, cancellation)?);
1474            report_progress(
1475                progress,
1476                "name_to_string_bulk",
1477                u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
1478                total,
1479                started,
1480            )?;
1481        }
1482        Ok(out)
1483    }
1484
1485    /// Enumerate the imported environment's declaration names and render
1486    /// each as a dotted string. Convenience over
1487    /// [`Self::list_declarations_filtered`] + [`Self::name_to_string_bulk`]
1488    /// for the common case where the consumer only needs strings.
1489    ///
1490    /// Two FFI hops (list + per-name render) and one heap allocation
1491    /// per name. For batches under a few thousand this is fine; for
1492    /// six-figure walks consider the lower-level pair so the listing
1493    /// pass and the rendering pass can be cancelled or chunked
1494    /// independently.
1495    ///
1496    /// # Errors
1497    ///
1498    /// Forwards errors from [`Self::list_declarations_filtered`] and
1499    /// [`Self::name_to_string_bulk`].
1500    pub fn list_declarations_strings(
1501        &mut self,
1502        filter: &LeanDeclarationFilter,
1503        cancellation: Option<&LeanCancellationToken>,
1504        progress: Option<&dyn LeanProgressSink>,
1505    ) -> LeanResult<Vec<String>> {
1506        let _span = tracing::debug_span!(target: "lean_rs", "lean_rs.host.session.list_declarations_strings").entered();
1507        let names = self.list_declarations_filtered(filter, cancellation, None)?;
1508        self.name_to_string_bulk(&names, cancellation, progress)
1509    }
1510
1511    /// Search declarations with structural filters and bounded metadata rows.
1512    ///
1513    /// The search runs inside Lean while traversing the imported environment.
1514    /// It may inspect declaration types structurally for required constants and
1515    /// conclusion heads, but it never renders type text. Use
1516    /// [`Self::declaration_type`] for explicit one-name type rendering.
1517    ///
1518    /// # Errors
1519    ///
1520    /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is already
1521    /// cancelled before dispatch. Returns [`lean_rs::LeanError::LeanException`]
1522    /// if the Lean-side search raises.
1523    pub fn search_declarations(
1524        &mut self,
1525        search: &DeclarationSearchRequest,
1526        cancellation: Option<&LeanCancellationToken>,
1527    ) -> LeanResult<DeclarationSearchResult> {
1528        let _span = tracing::debug_span!(
1529            target: "lean_rs",
1530            "lean_rs.host.session.search_declarations",
1531            limit = search.limit,
1532            include_source = search.include_source,
1533        )
1534        .entered();
1535        check_cancellation(cancellation)?;
1536        let source_roots = if search.include_source {
1537            self.capabilities
1538                .host()
1539                .project()
1540                .source_roots()?
1541                .into_iter()
1542                .map(|path| path.to_string_lossy().into_owned())
1543                .collect::<Vec<_>>()
1544        } else {
1545            Vec::new()
1546        };
1547        check_cancellation(cancellation)?;
1548        let t = Instant::now();
1549        let result = self
1550            .shims
1551            .env_search_declarations
1552            .call(self.environment.clone(), search.clone(), source_roots);
1553        self.record_call(0, t.elapsed());
1554        result
1555    }
1556
1557    /// Inspect one selected declaration under explicit output budgets.
1558    ///
1559    /// Search remains metadata-only; callers use this method after selecting
1560    /// one declaration name whose rendered statement/docstring are worth
1561    /// paying for. Missing names and missing optional shim support are normal
1562    /// result statuses.
1563    ///
1564    /// # Errors
1565    ///
1566    /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is already
1567    /// cancelled before dispatch. Returns [`lean_rs::LeanError::LeanException`]
1568    /// if the Lean-side inspection raises.
1569    pub fn inspect_declaration(
1570        &mut self,
1571        request: &DeclarationInspectionRequest,
1572        cancellation: Option<&LeanCancellationToken>,
1573    ) -> LeanResult<DeclarationInspectionResult> {
1574        let _span = tracing::debug_span!(
1575            target: "lean_rs",
1576            "lean_rs.host.session.inspect_declaration",
1577            source = request.fields.source,
1578            statement = request.fields.statement,
1579            docstring = request.fields.docstring,
1580            attributes = request.fields.attributes,
1581            flags = request.fields.flags,
1582        )
1583        .entered();
1584        check_cancellation(cancellation)?;
1585        let Some(inspect) = &self.shims.env_inspect_declaration else {
1586            return Ok(DeclarationInspectionResult::Unsupported);
1587        };
1588        let source_roots = if request.fields.source {
1589            self.capabilities
1590                .host()
1591                .project()
1592                .source_roots()?
1593                .into_iter()
1594                .map(|path| path.to_string_lossy().into_owned())
1595                .collect::<Vec<_>>()
1596        } else {
1597            Vec::new()
1598        };
1599        check_cancellation(cancellation)?;
1600        let t = Instant::now();
1601        // Bound the optional notation-aware statement rendering by the default
1602        // heartbeat budget; a deeply nested term that exceeds it falls back to
1603        // the raw `Expr.toString` form inside the shim.
1604        let result = inspect.call(
1605            self.environment.clone(),
1606            request.clone(),
1607            source_roots,
1608            lean_toolchain::LEAN_HEARTBEAT_LIMIT_DEFAULT,
1609        );
1610        self.record_call(0, t.elapsed());
1611        result
1612    }
1613
1614    /// Render `expr` via `Expr.toString`—the cheap, deterministic
1615    /// projection.
1616    ///
1617    /// Walks the syntax tree directly: no `MetaM`, no notation lookup,
1618    /// no binder pretty-printing. The result is a legible-but-ugly
1619    /// dump suitable for indexing, logging, and search keys. For the
1620    /// form a Lean user reads, use the optional
1621    /// [`crate::host::meta::pp_expr`] service through
1622    /// [`Self::run_meta`] instead—it pays for elaboration context to
1623    /// get notation and unfolding right but can time out under a tight
1624    /// heartbeat budget.
1625    ///
1626    /// # Errors
1627    ///
1628    /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
1629    /// already cancelled before dispatch.
1630    pub fn expr_to_string_raw(
1631        &mut self,
1632        expr: &LeanExpr<'lean>,
1633        cancellation: Option<&LeanCancellationToken>,
1634    ) -> LeanResult<String> {
1635        let _span = tracing::debug_span!(target: "lean_rs", "lean_rs.host.session.expr_to_string_raw").entered();
1636        check_cancellation(cancellation)?;
1637        let t = Instant::now();
1638        let result = self.shims.env_expr_to_string_raw.call(expr.clone());
1639        self.record_call(0, t.elapsed());
1640        result
1641    }
1642
1643    /// Parse and elaborate a Lean module, returning only the requested
1644    /// bounded projection.
1645    ///
1646    /// The Lean shim owns header parsing, module-system header handling,
1647    /// info-tree traversal, cursor selection, reference collection, and
1648    /// bounded expression/goal rendering. The Rust side chooses a
1649    /// [`ModuleQuery`] and receives the matching
1650    /// [`ModuleQueryOutcome`]; whole-file raw expression/type dumps never
1651    /// cross this boundary.
1652    ///
1653    /// The shim is optional. When the loaded capability dylib does not
1654    /// export `lean_rs_host_process_module_query`, the method returns
1655    /// [`ModuleQueryOutcome::Unsupported`] without an FFI call.
1656    ///
1657    /// # Errors
1658    ///
1659    /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
1660    /// already cancelled before dispatch. Returns
1661    /// [`lean_rs::LeanError::LeanException`] if the Lean-side shim
1662    /// raises through `IO`. Returns [`lean_rs::LeanError::Host`] with
1663    /// stage [`HostStage::Conversion`] if the Lean return value does
1664    /// not decode into [`ModuleQueryOutcome`].
1665    pub fn process_module_query(
1666        &mut self,
1667        source: &str,
1668        query: &ModuleQuery,
1669        options: &LeanElabOptions,
1670        cancellation: Option<&LeanCancellationToken>,
1671    ) -> LeanResult<ModuleQueryOutcome> {
1672        let _span = tracing::debug_span!(
1673            target: "lean_rs",
1674            "lean_rs.host.session.process_module_query",
1675            source_len = source.len(),
1676            heartbeats = options.heartbeats(),
1677            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1678        )
1679        .entered();
1680        check_cancellation(cancellation)?;
1681        let Some(call) = self.shims.process_module_query.as_ref() else {
1682            return Ok(ModuleQueryOutcome::Unsupported);
1683        };
1684        let t = Instant::now();
1685        let result = call.call(
1686            self.environment.clone(),
1687            source.to_owned(),
1688            query.clone(),
1689            options.namespace_context_str().to_owned(),
1690            options.file_label_str().to_owned(),
1691            options.heartbeats(),
1692            options.diagnostic_byte_limit_usize(),
1693        );
1694        self.record_call(0, t.elapsed());
1695        result
1696    }
1697
1698    /// Parse and elaborate a Lean module once, returning several bounded
1699    /// projections keyed by selector id.
1700    ///
1701    /// This is the proof-agent path: Lean owns header handling, one body
1702    /// elaboration, info-tree traversal, and selector projection. Rust sends
1703    /// a small selector array and receives per-selector outcomes; whole-file
1704    /// info-tree arrays never cross the boundary.
1705    ///
1706    /// The shim is optional. When the loaded capability dylib does not
1707    /// export `lean_rs_host_process_module_query_batch`, the method returns
1708    /// [`ModuleQueryBatchOutcome::Unsupported`] without an FFI call.
1709    ///
1710    /// # Errors
1711    ///
1712    /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
1713    /// already cancelled before dispatch. Returns
1714    /// [`lean_rs::LeanError::LeanException`] if the Lean-side shim raises
1715    /// through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
1716    /// [`HostStage::Conversion`] if the Lean return value does not decode
1717    /// into [`ModuleQueryBatchOutcome`].
1718    pub fn process_module_query_batch(
1719        &mut self,
1720        source: &str,
1721        selectors: &[ModuleQuerySelector],
1722        budgets: &ModuleQueryOutputBudgets,
1723        options: &LeanElabOptions,
1724        cancellation: Option<&LeanCancellationToken>,
1725    ) -> LeanResult<ModuleQueryBatchOutcome> {
1726        let _span = tracing::debug_span!(
1727            target: "lean_rs",
1728            "lean_rs.host.session.process_module_query_batch",
1729            source_len = source.len(),
1730            selectors = selectors.len(),
1731            per_field_bytes = budgets.per_field_bytes,
1732            total_bytes = budgets.total_bytes,
1733            heartbeats = options.heartbeats(),
1734            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1735        )
1736        .entered();
1737        check_cancellation(cancellation)?;
1738        let Some(call) = self.shims.process_module_query_batch.as_ref() else {
1739            return Ok(ModuleQueryBatchOutcome::Unsupported);
1740        };
1741        let selectors_owned = selectors.to_vec();
1742        let t = Instant::now();
1743        let result = call.call(
1744            self.environment.clone(),
1745            source.to_owned(),
1746            selectors_owned,
1747            budgets.clone(),
1748            options.namespace_context_str().to_owned(),
1749            options.file_label_str().to_owned(),
1750            options.heartbeats(),
1751            options.diagnostic_byte_limit_usize(),
1752        );
1753        self.record_call(u64::try_from(selectors.len()).unwrap_or(u64::MAX), t.elapsed());
1754        result
1755    }
1756
1757    /// Parse/elaborate a Lean module through the shim-owned module snapshot
1758    /// cache, then return bounded selector projections plus cache facts.
1759    ///
1760    /// The snapshot cache remains private to the loaded shim. Rust provides
1761    /// the stable cache key and conservative policy, but never receives raw
1762    /// info trees.
1763    ///
1764    /// # Errors
1765    ///
1766    /// Returns an error if cancellation is already requested, if the shim
1767    /// raises an `IO` exception, or if the Lean result cannot be decoded into
1768    /// the expected cached batch outcome.
1769    pub fn process_module_query_batch_cached(
1770        &mut self,
1771        source: &str,
1772        selectors: &[ModuleQuerySelector],
1773        budgets: &ModuleQueryOutputBudgets,
1774        options: &LeanElabOptions,
1775        policy: &ModuleQueryCachePolicy,
1776        cancellation: Option<&LeanCancellationToken>,
1777    ) -> LeanResult<ModuleQueryBatchCachedOutcome> {
1778        let _span = tracing::debug_span!(
1779            target: "lean_rs",
1780            "lean_rs.host.session.process_module_query_batch_cached",
1781            source_len = source.len(),
1782            selectors = selectors.len(),
1783            per_field_bytes = budgets.per_field_bytes,
1784            total_bytes = budgets.total_bytes,
1785            heartbeats = options.heartbeats(),
1786            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1787        )
1788        .entered();
1789        check_cancellation(cancellation)?;
1790        let Some(call) = self.shims.process_module_query_batch_cached.as_ref() else {
1791            return Ok(ModuleQueryBatchCachedOutcome::Unsupported);
1792        };
1793        let selectors_owned = selectors.to_vec();
1794        let policy_text = format!(
1795            "{}\n{}\n{}\n{}\n{}",
1796            policy.file_identity, policy.key, policy.max_entries, policy.ttl_millis, policy.max_bytes
1797        );
1798        let t = Instant::now();
1799        let result = call.call(
1800            self.environment.clone(),
1801            source.to_owned(),
1802            selectors_owned,
1803            budgets.clone(),
1804            options.namespace_context_str().to_owned(),
1805            options.file_label_str().to_owned(),
1806            options.heartbeats(),
1807            options.diagnostic_byte_limit_usize(),
1808            policy_text,
1809        );
1810        self.record_call(u64::try_from(selectors.len()).unwrap_or(u64::MAX), t.elapsed());
1811        result
1812    }
1813
1814    /// Try proof snippets against an in-memory source overlay.
1815    ///
1816    /// The shim is optional. When the loaded capability dylib does not export
1817    /// `lean_rs_host_attempt_proof`, the method returns
1818    /// [`ProofAttemptOutcome::Unsupported`] without an FFI call.
1819    ///
1820    /// # Errors
1821    ///
1822    /// Returns an error if cancellation is already requested, if the shim
1823    /// raises an `IO` exception, or if the Lean result cannot be decoded.
1824    pub fn attempt_proof(
1825        &mut self,
1826        request: &ProofAttemptRequest,
1827        options: &LeanElabOptions,
1828        cancellation: Option<&LeanCancellationToken>,
1829    ) -> LeanResult<ProofAttemptOutcome> {
1830        let _span = tracing::debug_span!(
1831            target: "lean_rs",
1832            "lean_rs.host.session.attempt_proof",
1833            source_len = request.source.len(),
1834            candidates = request.candidates.len(),
1835            per_field_bytes = request.budgets.per_field_bytes,
1836            total_bytes = request.budgets.total_bytes,
1837            heartbeats = options.heartbeats(),
1838            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1839        )
1840        .entered();
1841        check_cancellation(cancellation)?;
1842        let Some(call) = self.shims.attempt_proof.as_ref() else {
1843            return Ok(ProofAttemptOutcome::Unsupported);
1844        };
1845        let t = Instant::now();
1846        let result = call.call(
1847            self.environment.clone(),
1848            request.clone(),
1849            options.namespace_context_str().to_owned(),
1850            options.file_label_str().to_owned(),
1851            options.heartbeats(),
1852            options.diagnostic_byte_limit_usize(),
1853        );
1854        self.record_call(u64::try_from(request.candidates.len()).unwrap_or(u64::MAX), t.elapsed());
1855        result
1856    }
1857
1858    /// Verify one declaration in an in-memory source snapshot.
1859    ///
1860    /// The shim is optional. When the loaded capability dylib does not export
1861    /// `lean_rs_host_verify_declaration`, the method returns
1862    /// [`DeclarationVerificationOutcome::Unsupported`] without an FFI call.
1863    ///
1864    /// # Errors
1865    ///
1866    /// Returns an error if cancellation is already requested, if the shim
1867    /// raises an `IO` exception, or if the Lean result cannot be decoded.
1868    pub fn verify_declaration(
1869        &mut self,
1870        request: &DeclarationVerificationRequest,
1871        options: &LeanElabOptions,
1872        cancellation: Option<&LeanCancellationToken>,
1873    ) -> LeanResult<DeclarationVerificationOutcome> {
1874        let _span = tracing::debug_span!(
1875            target: "lean_rs",
1876            "lean_rs.host.session.verify_declaration",
1877            source_len = request.source.len(),
1878            report_axioms = request.report_axioms,
1879            per_field_bytes = request.budgets.per_field_bytes,
1880            total_bytes = request.budgets.total_bytes,
1881            heartbeats = options.heartbeats(),
1882            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1883        )
1884        .entered();
1885        check_cancellation(cancellation)?;
1886        let Some(call) = self.shims.verify_declaration.as_ref() else {
1887            return Ok(DeclarationVerificationOutcome::Unsupported);
1888        };
1889        let t = Instant::now();
1890        let result = call.call(
1891            self.environment.clone(),
1892            request.clone(),
1893            options.namespace_context_str().to_owned(),
1894            options.file_label_str().to_owned(),
1895            options.heartbeats(),
1896            options.diagnostic_byte_limit_usize(),
1897        );
1898        self.record_call(1, t.elapsed());
1899        result
1900    }
1901
1902    /// Verify several declarations in one in-memory source snapshot.
1903    ///
1904    /// The shim is optional. When the loaded capability dylib does not export
1905    /// `lean_rs_host_verify_declaration_batch`, the method returns
1906    /// [`DeclarationVerificationBatchOutcome::Unsupported`] without an FFI
1907    /// call. The returned rows preserve request order.
1908    ///
1909    /// # Errors
1910    ///
1911    /// Returns an error if cancellation is already requested, if the shim
1912    /// raises an `IO` exception, or if the Lean result cannot be decoded.
1913    pub fn verify_declaration_batch(
1914        &mut self,
1915        request: &DeclarationVerificationBatchRequest,
1916        options: &LeanElabOptions,
1917        cancellation: Option<&LeanCancellationToken>,
1918    ) -> LeanResult<DeclarationVerificationBatchOutcome> {
1919        let _span = tracing::debug_span!(
1920            target: "lean_rs",
1921            "lean_rs.host.session.verify_declaration_batch",
1922            source_len = request.source.len(),
1923            targets = request.targets.len(),
1924            report_axioms = request.report_axioms,
1925            per_field_bytes = request.budgets.per_field_bytes,
1926            total_bytes = request.budgets.total_bytes,
1927            heartbeats = options.heartbeats(),
1928            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1929        )
1930        .entered();
1931        check_cancellation(cancellation)?;
1932        let Some(call) = self.shims.verify_declaration_batch.as_ref() else {
1933            return Ok(DeclarationVerificationBatchOutcome::Unsupported);
1934        };
1935        let t = Instant::now();
1936        let result = call.call(
1937            self.environment.clone(),
1938            request.clone(),
1939            options.namespace_context_str().to_owned(),
1940            options.file_label_str().to_owned(),
1941            options.heartbeats(),
1942            options.diagnostic_byte_limit_usize(),
1943        );
1944        self.record_call(u64::try_from(request.targets.len()).unwrap_or(u64::MAX), t.elapsed());
1945        result
1946    }
1947
1948    /// Clear the shim-owned module snapshot cache when the loaded capability
1949    /// supports it.
1950    ///
1951    /// This releases only cached module snapshots built by the bundled
1952    /// info-tree shim. It does not reset Lean's runtime, unload imported
1953    /// modules, or make full-session compacted regions safe to free.
1954    ///
1955    /// # Errors
1956    ///
1957    /// Returns an error if the shim raises an `IO` exception or if the Lean
1958    /// clear result cannot be decoded.
1959    pub fn clear_module_snapshot_cache(&mut self) -> LeanResult<ModuleSnapshotCacheClearResult> {
1960        let Some(call) = self.shims.clear_module_snapshot_cache.as_ref() else {
1961            return Ok(ModuleSnapshotCacheClearResult {
1962                entries_cleared: 0,
1963                approx_bytes_cleared: 0,
1964            });
1965        };
1966        let t = Instant::now();
1967        let result = call.call();
1968        self.record_call(0, t.elapsed());
1969        result
1970    }
1971
1972    /// Parse and elaborate a single Lean term against the imported
1973    /// environment, optionally against an expected type.
1974    ///
1975    /// The boundary is explicit: Rust supplies the source text, module
1976    /// context, and bounded options; Lean parses, elaborates, and
1977    /// returns either an opaque [`LeanExpr`] handle or a structured
1978    /// [`LeanElabFailure`] carrying typed diagnostics. Rust does not
1979    /// inspect elaborator internals or proof terms to decide
1980    /// correctness.
1981    ///
1982    /// The outer [`LeanResult`] surfaces host-stack failures (a Lean
1983    /// `IO`-level exception from the shim itself, a malformed Lean
1984    /// return value); the inner `Result` distinguishes successful
1985    /// elaboration from parse / type / kernel-stage failures the
1986    /// elaborator reports through its `MessageLog`. Both error paths
1987    /// propagate the [`LeanElabOptions::diagnostic_byte_limit`] bound
1988    /// structurally.
1989    ///
1990    /// # Errors
1991    ///
1992    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side shim raises
1993    /// through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
1994    /// [`HostStage::Conversion`] if the Lean return value does not
1995    /// decode into [`LeanElabFailure`] / [`LeanExpr`].
1996    pub fn elaborate(
1997        &mut self,
1998        source: &str,
1999        expected_type: Option<&LeanExpr<'lean>>,
2000        options: &LeanElabOptions,
2001        cancellation: Option<&LeanCancellationToken>,
2002    ) -> LeanResult<Result<LeanExpr<'lean>, LeanElabFailure>> {
2003        let _span = tracing::debug_span!(
2004            target: "lean_rs",
2005            "lean_rs.host.session.elaborate",
2006            source_len = source.len(),
2007            heartbeats = options.heartbeats(),
2008            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
2009        )
2010        .entered();
2011        check_cancellation(cancellation)?;
2012        let t = Instant::now();
2013        let result = self.shims.elaborate.call(
2014            self.environment.clone(),
2015            source.to_owned(),
2016            expected_type.cloned(),
2017            options.namespace_context_str().to_owned(),
2018            options.file_label_str().to_owned(),
2019            options.heartbeats(),
2020            options.diagnostic_byte_limit_usize(),
2021        );
2022        self.record_call(0, t.elapsed());
2023        result
2024    }
2025
2026    /// Parse, elaborate, and kernel-check a Lean declaration source
2027    /// (typically a `theorem` or `def`), returning a typed outcome
2028    /// that classifies the result and carries either the produced
2029    /// [`crate::LeanEvidence`] handle or the diagnostics the elaborator and
2030    /// kernel emitted.
2031    ///
2032    /// The boundary is explicit (mirrors [`Self::elaborate`]): Rust
2033    /// supplies source + options; Lean parses, elaborates, runs
2034    /// `addDecl` (which kernel-checks), and classifies the outcome.
2035    /// Rust never inspects the produced proof term or declaration
2036    /// internals.
2037    ///
2038    /// # Errors
2039    ///
2040    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side shim
2041    /// raises through `IO` (an unexpected internal failure that is not
2042    /// itself a rejection / unavailable diagnostic). Returns
2043    /// [`lean_rs::LeanError::Host`] with stage [`HostStage::Conversion`] if the
2044    /// Lean return value does not decode into [`LeanKernelOutcome`].
2045    pub fn kernel_check(
2046        &mut self,
2047        source: &str,
2048        options: &LeanElabOptions,
2049        cancellation: Option<&LeanCancellationToken>,
2050        progress: Option<&dyn LeanProgressSink>,
2051    ) -> LeanResult<LeanKernelOutcome<'lean>> {
2052        let _span = tracing::debug_span!(
2053            target: "lean_rs",
2054            "lean_rs.host.session.kernel_check",
2055            source_len = source.len(),
2056            heartbeats = options.heartbeats(),
2057            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
2058        )
2059        .entered();
2060        check_cancellation(cancellation)?;
2061        if let Some(sink) = progress {
2062            let bridge = ProgressBridge::new(sink, "kernel_check", Some(1))?;
2063            let (handle, trampoline) = bridge.abi_parts();
2064            let t = Instant::now();
2065            let result = self.shims.kernel_check_progress.call(
2066                self.environment.clone(),
2067                source.to_owned(),
2068                options.namespace_context_str().to_owned(),
2069                options.file_label_str().to_owned(),
2070                options.heartbeats(),
2071                options.diagnostic_byte_limit_usize(),
2072                handle,
2073                trampoline,
2074            );
2075            self.record_call(0, t.elapsed());
2076            bridge.decode(result?)
2077        } else {
2078            let t = Instant::now();
2079            let result = self.shims.kernel_check.call(
2080                self.environment.clone(),
2081                source.to_owned(),
2082                options.namespace_context_str().to_owned(),
2083                options.file_label_str().to_owned(),
2084                options.heartbeats(),
2085                options.diagnostic_byte_limit_usize(),
2086            );
2087            self.record_call(0, t.elapsed());
2088            result
2089        }
2090    }
2091
2092    /// Re-validate a previously captured [`LeanEvidence`] against the
2093    /// session's imported environment, returning the kernel's current
2094    /// verdict.
2095    ///
2096    /// The handle was produced by an earlier
2097    /// [`Self::kernel_check`] call against this same environment and
2098    /// carries the kernel-accepted `Lean.Declaration` opaquely. The
2099    /// session never installs that declaration into its stored
2100    /// environment, so re-checking against the unchanged environment
2101    /// is the supported way to ask "is this evidence still valid?"—
2102    /// the kernel runs fresh.
2103    ///
2104    /// The returned [`EvidenceStatus`] mirrors
2105    /// [`LeanKernelOutcome::status`]: `Checked` on success, `Rejected`
2106    /// if the kernel now refuses the declaration, `Unavailable` if
2107    /// the Lean shim caught an `IO` exception. The Lean fixture does
2108    /// not currently emit `Unsupported` from this path—`Unsupported`
2109    /// only fires during the initial classification in
2110    /// `kernel_check`.
2111    ///
2112    /// # Errors
2113    ///
2114    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean shim raises
2115    /// through `IO` outside of its own `try` (an unexpected internal
2116    /// failure that the shim did not classify). Returns
2117    /// [`lean_rs::LeanError::Host`] with stage [`HostStage::Conversion`] if the
2118    /// return value does not decode as a four-tag
2119    /// [`EvidenceStatus`] inductive.
2120    pub fn check_evidence(
2121        &mut self,
2122        handle: &LeanEvidence<'lean>,
2123        cancellation: Option<&LeanCancellationToken>,
2124    ) -> LeanResult<EvidenceStatus> {
2125        let _span = tracing::debug_span!(
2126            target: "lean_rs",
2127            "lean_rs.host.session.check_evidence",
2128        )
2129        .entered();
2130        check_cancellation(cancellation)?;
2131        let t = Instant::now();
2132        let result = self.shims.check_evidence.call(self.environment.clone(), handle.clone());
2133        self.record_call(0, t.elapsed());
2134        result
2135    }
2136
2137    /// Project a previously captured [`LeanEvidence`] into a bounded
2138    /// [`ProofSummary`] for diagnostics or storage.
2139    ///
2140    /// The Lean shim renders the captured declaration's name, kind,
2141    /// and type expression as three byte-bounded `String`s—no
2142    /// `Lean.Expr` or proof term crosses the FFI boundary. The
2143    /// summary is computed on demand (not at
2144    /// [`Self::kernel_check`] time) because most callers only ever
2145    /// inspect the [`EvidenceStatus`] tag and would pay the
2146    /// pretty-print cost for nothing.
2147    ///
2148    /// Strings on the returned summary are display text. They are not
2149    /// semantic keys; route equality comparisons through a
2150    /// Lean-authored equality export.
2151    ///
2152    /// # Errors
2153    ///
2154    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean shim raises
2155    /// through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
2156    /// [`HostStage::Conversion`] if the return value does not decode
2157    /// as a three-field [`ProofSummary`] structure.
2158    pub fn summarize_evidence(
2159        &mut self,
2160        handle: &LeanEvidence<'lean>,
2161        cancellation: Option<&LeanCancellationToken>,
2162    ) -> LeanResult<ProofSummary> {
2163        let _span = tracing::debug_span!(
2164            target: "lean_rs",
2165            "lean_rs.host.session.summarize_evidence",
2166        )
2167        .entered();
2168        check_cancellation(cancellation)?;
2169        let t = Instant::now();
2170        let result = self
2171            .shims
2172            .evidence_summary
2173            .call(self.environment.clone(), handle.clone());
2174        self.record_call(0, t.elapsed());
2175        result
2176    }
2177
2178    /// Invoke a registered bounded [`MetaM`](https://leanprover.github.io/theorem_proving_in_lean4/)
2179    /// service against the imported environment.
2180    ///
2181    /// The session dispatches through the checked binding for the closed
2182    /// service shape; if the loaded capability does not export the optional
2183    /// symbol, the call short-circuits to [`LeanMetaResponse::Unsupported`]
2184    /// with a synthetic host-side diagnostic naming the missing symbol.
2185    ///
2186    /// The outer [`LeanResult`] surfaces host-stack failures (a Lean
2187    /// `IO`-level exception from the shim itself, or an undecodable
2188    /// return value). The four-way classification—`Ok` / `Failed` /
2189    /// `TimeoutOrHeartbeat` / `Unsupported`—lives in the inner
2190    /// [`LeanMetaResponse`].
2191    ///
2192    /// # Errors
2193    ///
2194    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean shim raises
2195    /// through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
2196    /// [`HostStage::Conversion`] if the return value does not decode
2197    /// into [`LeanMetaResponse<Resp>`].
2198    pub fn run_meta<Req, Resp>(
2199        &mut self,
2200        service: &LeanMetaService<Req, Resp>,
2201        request: Req,
2202        options: &LeanMetaOptions,
2203        cancellation: Option<&LeanCancellationToken>,
2204    ) -> LeanResult<LeanMetaResponse<Resp>>
2205    where
2206        LeanMetaService<Req, Resp>: HostMetaDispatch<'lean, Req, Resp>,
2207        Req: lean_rs::abi::traits::LeanAbi<'lean>,
2208        Resp: TryFromLean<'lean>,
2209    {
2210        let _span = tracing::debug_span!(
2211            target: "lean_rs",
2212            "lean_rs.host.session.run_meta",
2213            service = service.name(),
2214            heartbeats = options.heartbeats(),
2215            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
2216        )
2217        .entered();
2218        check_cancellation(cancellation)?;
2219        service.dispatch(self, request, options)
2220    }
2221
2222    /// Look up many declarations in one Lean traversal.
2223    ///
2224    /// Equivalent to calling [`Self::query_declaration`] in a loop over
2225    /// `names`, except that the entire batch crosses the FFI boundary
2226    /// exactly once: one `Array Name` allocation in, one
2227    /// `Array (Option Declaration)` allocation out. The Lean shim folds
2228    /// the singular `envQueryDeclaration` across the input array, so the
2229    /// iteration semantics are identical to a Rust-side fold over the
2230    /// singular path—a missing name still errors the batch.
2231    ///
2232    /// Names are still resolved through the capability's
2233    /// `name_from_string` shim, one [`lean_rs::LeanName`] handle per
2234    /// input. The metric impact is `names.len() + 1` recorded FFI calls
2235    /// for a batch of `names.len()` items, versus `2 * names.len()` for
2236    /// the same workload through [`Self::query_declaration`].
2237    ///
2238    /// # Errors
2239    ///
2240    /// Returns [`lean_rs::LeanError::Host`] with stage [`HostStage::Conversion`]
2241    /// on the first name that is not present in the imported
2242    /// environment, with the missing name in the diagnostic. Returns
2243    /// [`lean_rs::LeanError::LeanException`] if the Lean-side bulk shim raises
2244    /// through `IO`.
2245    pub fn query_declarations_bulk(
2246        &mut self,
2247        names: &[&str],
2248        cancellation: Option<&LeanCancellationToken>,
2249        progress: Option<&dyn LeanProgressSink>,
2250    ) -> LeanResult<Vec<LeanDeclaration<'lean>>> {
2251        let _span = tracing::debug_span!(
2252            target: "lean_rs",
2253            "lean_rs.host.session.query_declarations_bulk",
2254            batch_size = names.len(),
2255        )
2256        .entered();
2257        if names.is_empty() {
2258            return Ok(Vec::new());
2259        }
2260        check_cancellation(cancellation)?;
2261        if cancellation.is_some() {
2262            let started = Instant::now();
2263            let mut out = Vec::with_capacity(names.len());
2264            let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
2265            for (idx, name) in names.iter().enumerate() {
2266                check_cancellation(cancellation)?;
2267                out.push(self.query_declaration(name, cancellation)?);
2268                report_progress(
2269                    progress,
2270                    "query_declarations_bulk",
2271                    u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
2272                    total,
2273                    started,
2274                )?;
2275            }
2276            return Ok(out);
2277        }
2278        let prepare_started = Instant::now();
2279        let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
2280        let mut name_handles: Vec<LeanName<'lean>> = Vec::with_capacity(names.len());
2281        for (idx, name) in names.iter().enumerate() {
2282            name_handles.push(self.make_name(name, cancellation)?);
2283            report_progress(
2284                progress,
2285                "prepare_names",
2286                u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
2287                total,
2288                prepare_started,
2289            )?;
2290        }
2291        check_cancellation(cancellation)?;
2292        let raw = if let Some(sink) = progress {
2293            let bridge = ProgressBridge::new(sink, "query_declarations_bulk", total)?;
2294            let (handle, trampoline) = bridge.abi_parts();
2295            let t = Instant::now();
2296            let result = self.shims.env_query_declarations_bulk_progress.call(
2297                self.environment.clone(),
2298                name_handles,
2299                handle,
2300                trampoline,
2301            );
2302            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
2303            self.record_call(batch_len, t.elapsed());
2304            bridge.decode::<Vec<Option<LeanDeclaration<'lean>>>>(result?)?
2305        } else {
2306            let t = Instant::now();
2307            let result = self
2308                .shims
2309                .env_query_declarations_bulk
2310                .call(self.environment.clone(), name_handles);
2311            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
2312            self.record_call(batch_len, t.elapsed());
2313            result?
2314        };
2315        let mut out: Vec<LeanDeclaration<'lean>> = Vec::with_capacity(raw.len());
2316        for (slot, name) in raw.into_iter().zip(names.iter()) {
2317            match slot {
2318                Some(decl) => out.push(decl),
2319                None => {
2320                    return Err(lean_rs::abi::traits::conversion_error(format!(
2321                        "declaration '{name}' not found in imported environment"
2322                    )));
2323                }
2324            }
2325        }
2326        Ok(out)
2327    }
2328
2329    /// Parse and elaborate many independent Lean terms in one Lean
2330    /// traversal.
2331    ///
2332    /// Per-source `Result<LeanExpr, LeanElabFailure>` shape matches
2333    /// [`Self::elaborate`] exactly: outer [`LeanResult`] surfaces
2334    /// host-stack failures, inner per-source `Result` distinguishes
2335    /// successful elaboration from elaborator-reported diagnostics. A
2336    /// caller treating the bulk path as a fold over the singular path
2337    /// sees no semantic surprise.
2338    ///
2339    /// The `expected_type` parameter is **not** carried by the bulk
2340    /// shape: per-source expectations would force a parallel
2341    /// `&[Option<&LeanExpr>]` array, and no in-tree caller has earned
2342    /// the surface. Use [`Self::elaborate`] for individual terms with
2343    /// expected types.
2344    ///
2345    /// The heartbeat and diagnostic-byte budgets in `options` apply
2346    /// once each per source (the Lean shim builds fresh
2347    /// [`Lean.Options`](https://leanprover.github.io/) per item via the
2348    /// same `hostElaborate` path), so the per-batch upper bound on
2349    /// elapsed CPU work is `sources.len() * options.heartbeats()`.
2350    ///
2351    /// # Errors
2352    ///
2353    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side bulk shim
2354    /// raises through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
2355    /// [`HostStage::Conversion`] if the Lean return value does not
2356    /// decode into a `Vec<Result<LeanExpr, LeanElabFailure>>`.
2357    pub fn elaborate_bulk(
2358        &mut self,
2359        sources: &[&str],
2360        options: &LeanElabOptions,
2361        cancellation: Option<&LeanCancellationToken>,
2362        progress: Option<&dyn LeanProgressSink>,
2363    ) -> LeanResult<Vec<Result<LeanExpr<'lean>, LeanElabFailure>>> {
2364        let _span = tracing::debug_span!(
2365            target: "lean_rs",
2366            "lean_rs.host.session.elaborate_bulk",
2367            batch_size = sources.len(),
2368            heartbeats = options.heartbeats(),
2369            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
2370        )
2371        .entered();
2372        if sources.is_empty() {
2373            return Ok(Vec::new());
2374        }
2375        check_cancellation(cancellation)?;
2376        if cancellation.is_some() {
2377            let started = Instant::now();
2378            let total = Some(u64::try_from(sources.len()).unwrap_or(u64::MAX));
2379            let mut out = Vec::with_capacity(sources.len());
2380            for (idx, source) in sources.iter().enumerate() {
2381                check_cancellation(cancellation)?;
2382                out.push(self.elaborate(source, None, options, cancellation)?);
2383                report_progress(
2384                    progress,
2385                    "elaborate_bulk",
2386                    u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
2387                    total,
2388                    started,
2389                )?;
2390            }
2391            return Ok(out);
2392        }
2393        let sources_owned: Vec<String> = sources.iter().map(|&s| s.to_owned()).collect();
2394        if let Some(sink) = progress {
2395            let total = Some(u64::try_from(sources.len()).unwrap_or(u64::MAX));
2396            let bridge = ProgressBridge::new(sink, "elaborate_bulk", total)?;
2397            let (handle, trampoline) = bridge.abi_parts();
2398            let t = Instant::now();
2399            let result = self.shims.elaborate_bulk_progress.call(
2400                self.environment.clone(),
2401                sources_owned,
2402                options.namespace_context_str().to_owned(),
2403                options.file_label_str().to_owned(),
2404                options.heartbeats(),
2405                options.diagnostic_byte_limit_usize(),
2406                handle,
2407                trampoline,
2408            );
2409            let batch_len = u64::try_from(sources.len()).unwrap_or(u64::MAX);
2410            self.record_call(batch_len, t.elapsed());
2411            bridge.decode(result?)
2412        } else {
2413            let t = Instant::now();
2414            let result = self.shims.elaborate_bulk.call(
2415                self.environment.clone(),
2416                sources_owned,
2417                options.namespace_context_str().to_owned(),
2418                options.file_label_str().to_owned(),
2419                options.heartbeats(),
2420                options.diagnostic_byte_limit_usize(),
2421            );
2422            let batch_len = u64::try_from(sources.len()).unwrap_or(u64::MAX);
2423            self.record_call(batch_len, t.elapsed());
2424            result
2425        }
2426    }
2427
2428    /// Build a `LeanName` from a dotted Rust string via the capability's
2429    /// `Name.toName` shim.
2430    fn make_name(&self, name: &str, cancellation: Option<&LeanCancellationToken>) -> LeanResult<LeanName<'lean>> {
2431        check_cancellation(cancellation)?;
2432        let lean_name = lean_rs::__host_internals::string_from_str(self.capabilities.host().runtime(), name);
2433        let t = Instant::now();
2434        let result = self.shims.name_from_string.call(lean_name);
2435        self.record_call(0, t.elapsed());
2436        result
2437    }
2438}
2439
2440trait HostMetaDispatch<'lean, Req, Resp> {
2441    fn dispatch(
2442        &self,
2443        session: &mut LeanSession<'lean, '_>,
2444        request: Req,
2445        options: &LeanMetaOptions,
2446    ) -> LeanResult<LeanMetaResponse<Resp>>;
2447}
2448
2449impl<'lean> HostMetaDispatch<'lean, LeanExpr<'lean>, LeanExpr<'lean>>
2450    for LeanMetaService<LeanExpr<'lean>, LeanExpr<'lean>>
2451{
2452    fn dispatch(
2453        &self,
2454        session: &mut LeanSession<'lean, '_>,
2455        request: LeanExpr<'lean>,
2456        options: &LeanMetaOptions,
2457    ) -> LeanResult<LeanMetaResponse<LeanExpr<'lean>>> {
2458        let Some(call) = (match self.name() {
2459            "lean_rs_host_meta_infer_type" => session.shims.meta_infer_type.as_ref(),
2460            "lean_rs_host_meta_whnf" => session.shims.meta_whnf.as_ref(),
2461            "lean_rs_host_meta_heartbeat_burn" => session.shims.meta_heartbeat_burn.as_ref(),
2462            _ => None,
2463        }) else {
2464            return Ok(unsupported_meta_response(self.name()));
2465        };
2466        let t = Instant::now();
2467        let result = call.call(
2468            session.environment.clone(),
2469            request,
2470            options.heartbeats(),
2471            options.diagnostic_byte_limit_usize(),
2472            options.transparency_byte(),
2473        );
2474        session.record_call(0, t.elapsed());
2475        result
2476    }
2477}
2478
2479impl<'lean>
2480    HostMetaDispatch<
2481        'lean,
2482        (
2483            LeanExpr<'lean>,
2484            LeanExpr<'lean>,
2485            crate::host::meta::LeanMetaTransparency,
2486        ),
2487        bool,
2488    >
2489    for LeanMetaService<
2490        (
2491            LeanExpr<'lean>,
2492            LeanExpr<'lean>,
2493            crate::host::meta::LeanMetaTransparency,
2494        ),
2495        bool,
2496    >
2497{
2498    fn dispatch(
2499        &self,
2500        session: &mut LeanSession<'lean, '_>,
2501        request: (
2502            LeanExpr<'lean>,
2503            LeanExpr<'lean>,
2504            crate::host::meta::LeanMetaTransparency,
2505        ),
2506        options: &LeanMetaOptions,
2507    ) -> LeanResult<LeanMetaResponse<bool>> {
2508        let Some(call) = session.shims.meta_is_def_eq.as_ref() else {
2509            return Ok(unsupported_meta_response(self.name()));
2510        };
2511        let t = Instant::now();
2512        let result = call.call(
2513            session.environment.clone(),
2514            request,
2515            options.heartbeats(),
2516            options.diagnostic_byte_limit_usize(),
2517            options.transparency_byte(),
2518        );
2519        session.record_call(0, t.elapsed());
2520        result
2521    }
2522}
2523
2524impl<'lean> HostMetaDispatch<'lean, LeanExpr<'lean>, String> for LeanMetaService<LeanExpr<'lean>, String> {
2525    fn dispatch(
2526        &self,
2527        session: &mut LeanSession<'lean, '_>,
2528        request: LeanExpr<'lean>,
2529        options: &LeanMetaOptions,
2530    ) -> LeanResult<LeanMetaResponse<String>> {
2531        let Some(call) = session.shims.meta_pp_expr.as_ref() else {
2532            return Ok(unsupported_meta_response(self.name()));
2533        };
2534        let t = Instant::now();
2535        let result = call.call(
2536            session.environment.clone(),
2537            request,
2538            options.heartbeats(),
2539            options.diagnostic_byte_limit_usize(),
2540            options.transparency_byte(),
2541        );
2542        session.record_call(0, t.elapsed());
2543        result
2544    }
2545}
2546
2547fn unsupported_meta_response<Resp>(symbol: &str) -> LeanMetaResponse<Resp> {
2548    LeanMetaResponse::Unsupported(LeanElabFailure::synthetic(
2549        format!("bundled host shim does not export meta service '{symbol}'"),
2550        "<lean-rs-host meta>".to_owned(),
2551    ))
2552}