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