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