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 search_paths: Vec<String> = vec![
483            project.olean_search_path().to_string_lossy().into_owned(),
484            crate::host::lake::LakeProject::interop_olean_search_path()?
485                .to_string_lossy()
486                .into_owned(),
487            crate::host::lake::LakeProject::shim_olean_search_path()?
488                .to_string_lossy()
489                .into_owned(),
490        ];
491        let imports_owned: Vec<String> = imports.iter().map(|&s| s.to_owned()).collect();
492        // Lean 4.30 strictly enforces `enableInitializersExecution` before
493        // `importModules (loadExts := true)`. The flag is process-global,
494        // but `Lean.withImporting` (wrapped around every import) resets it
495        // on completion — two threads importing concurrently race the
496        // shim's enable→import sequence and the loser sees the flag
497        // cleared by the winner's reset. Serializing the import phase
498        // across the process matches Lean's "single execution thread
499        // accessing the global references" requirement. Sessions operate
500        // concurrently on their own `Environment` values once import
501        // returns; the lock spans only the FFI call.
502        let _import_guard = SESSION_IMPORT_LOCK
503            .lock()
504            .unwrap_or_else(|poisoned| poisoned.into_inner());
505        let environment = if let Some(sink) = progress {
506            let bridge = ProgressBridge::new(sink, "import", Some(u64::try_from(imports.len()).unwrap_or(u64::MAX)))?;
507            let (handle, trampoline) = bridge.abi_parts();
508            let address = capabilities.symbols().session_import_progress;
509            // SAFETY: `address` was resolved by `SessionSymbols::resolve`
510            // against the shim library, which outlives `'c`. The
511            // signature `(Array String, Array String, USize, USize) ->
512            // IO (Except UInt8 Environment)` matches the Lean-side
513            // progress import shim.
514            let import_fn: LeanExported<'lean, '_, (Vec<String>, Vec<String>, usize, usize), LeanIo<Obj<'lean>>> =
515                unsafe { LeanExported::from_function_address(runtime, address) };
516            let raw = import_fn.call(search_paths, imports_owned, handle, trampoline)?;
517            bridge.decode(raw)?
518        } else {
519            let address = capabilities.symbols().session_import;
520            // SAFETY: `address` was resolved by `SessionSymbols::resolve`
521            // against the shim library, which outlives `'c`. The
522            // signature `(Vec<String>, Vec<String>) -> IO Environment`
523            // matches the Lean-side `lean_rs_host_session_import` —
524            // first argument is the list of `.olean` search-path
525            // entries (the user's package + the shim package), second
526            // is the module-name list.
527            let import_fn: LeanExported<'lean, '_, (Vec<String>, Vec<String>), LeanIo<Obj<'lean>>> =
528                unsafe { LeanExported::from_function_address(runtime, address) };
529            import_fn.call(search_paths, imports_owned)?
530        };
531        Ok(Self {
532            capabilities,
533            environment,
534            stats: Cell::new(SessionStats::default()),
535        })
536    }
537
538    /// Wrap a previously-imported `Lean.Environment` as a fresh
539    /// [`LeanSession`] over `capabilities`.
540    ///
541    /// Crate-private; only [`crate::host::pool::SessionPool::acquire`]
542    /// calls this to recycle a pooled environment under a new
543    /// capability borrow. The returned session's [`SessionStats`] start
544    /// at zero — accumulated counters from the previous owner do not
545    /// leak across pool checkouts.
546    pub(crate) fn from_environment(capabilities: &'c LeanCapabilities<'lean, 'c>, environment: Obj<'lean>) -> Self {
547        Self {
548            capabilities,
549            environment,
550            stats: Cell::new(SessionStats::default()),
551        }
552    }
553
554    /// Consume the session and return its owned `Lean.Environment`.
555    ///
556    /// Crate-private; only [`crate::host::pool::SessionPool`] uses this
557    /// to reclaim the environment when a [`crate::host::pool::PooledSession`]
558    /// drops. The returned `Obj<'lean>` carries one Lean refcount, which
559    /// the pool is responsible for either pushing back into the free
560    /// list (in which case `Drop` runs later when the pool itself
561    /// drops) or releasing immediately (when at capacity).
562    pub(crate) fn into_environment(self) -> Obj<'lean> {
563        self.environment
564    }
565
566    /// Snapshot of this session's accumulated dispatch metrics.
567    ///
568    /// Returns a copy; the counters keep accumulating after the call.
569    /// Use [`SessionStats::default`] to compute a delta across two
570    /// snapshots.
571    #[must_use]
572    pub fn stats(&self) -> SessionStats {
573        self.stats.get()
574    }
575
576    /// Internal helper: record one FFI call and add `batch` per-item
577    /// entries plus `elapsed` nanoseconds. Singular methods pass
578    /// `batch = 0`; bulk methods pass the input length.
579    fn record_call(&self, batch: u64, elapsed: std::time::Duration) {
580        let mut s = self.stats.get();
581        s.ffi_calls = s.ffi_calls.saturating_add(1);
582        s.batch_items = s.batch_items.saturating_add(batch);
583        s.elapsed_ns = s
584            .elapsed_ns
585            .saturating_add(u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX));
586        self.stats.set(s);
587    }
588
589    fn decode_strings_cached(raw: Vec<Obj<'lean>>) -> LeanResult<Vec<String>> {
590        if raw.is_empty() {
591            return Ok(Vec::new());
592        }
593        let Some(first_key) = raw.first().map(Obj::as_raw_borrowed) else {
594            return Ok(Vec::new());
595        };
596        if raw.iter().all(|obj| obj.as_raw_borrowed() == first_key) {
597            let len = raw.len();
598            let mut raw_iter = raw.into_iter();
599            let Some(first) = raw_iter.next() else {
600                return Ok(Vec::new());
601            };
602            let value = String::try_from_lean(first)?;
603            return Ok(vec![value; len]);
604        }
605        let mut out = Vec::with_capacity(raw.len());
606        for obj in raw {
607            out.push(String::try_from_lean(obj)?);
608        }
609        Ok(out)
610    }
611
612    fn all_equal_name<'a>(names: &'a [&str]) -> Option<&'a str> {
613        let first = *names.first()?;
614        names.iter().all(|name| *name == first).then_some(first)
615    }
616
617    /// Look up a declaration by full Lean name (e.g. `"Nat.zero"`).
618    ///
619    /// # Errors
620    ///
621    /// Returns [`lean_rs::LeanError::Host`] with stage [`HostStage::Conversion`]
622    /// if the name is not present in the imported environment. Returns
623    /// [`lean_rs::LeanError::LeanException`] if the Lean-side query raises.
624    pub fn query_declaration(
625        &mut self,
626        name: &str,
627        cancellation: Option<&LeanCancellationToken>,
628    ) -> LeanResult<LeanDeclaration<'lean>> {
629        let _span = tracing::debug_span!(
630            target: "lean_rs",
631            "lean_rs.host.session.query_declaration",
632            name = name,
633        )
634        .entered();
635        check_cancellation(cancellation)?;
636        let name_handle = self.make_name(name, cancellation)?;
637        check_cancellation(cancellation)?;
638        let address = self.capabilities.symbols().env_query_declaration;
639        // SAFETY: per the SessionSymbols::resolve invariant; signature
640        // is `(Environment, Name) -> IO (Option Declaration)`.
641        let query: LeanExported<'lean, '_, (Obj<'lean>, LeanName<'lean>), LeanIo<Option<LeanDeclaration<'lean>>>> =
642            unsafe { LeanExported::from_function_address(self.runtime(), address) };
643        let t = Instant::now();
644        let result = query.call(self.environment.clone(), name_handle);
645        self.record_call(0, t.elapsed());
646        match result? {
647            Some(decl) => Ok(decl),
648            None => Err(lean_rs::abi::traits::conversion_error(format!(
649                "declaration '{name}' not found in imported environment"
650            ))),
651        }
652    }
653
654    /// All declaration names in the imported environment.
655    ///
656    /// Returns a Vec; the environment's `constants` map contains many
657    /// thousands of entries even for a small project (Lean prelude is
658    /// always imported), so prefer [`LeanSession::query_declaration`]
659    /// when you already know the name.
660    ///
661    /// # Errors
662    ///
663    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side query
664    /// raises.
665    pub fn list_declarations(
666        &mut self,
667        cancellation: Option<&LeanCancellationToken>,
668    ) -> LeanResult<Vec<LeanName<'lean>>> {
669        let _span = tracing::debug_span!(
670            target: "lean_rs",
671            "lean_rs.host.session.list_declarations",
672        )
673        .entered();
674        check_cancellation(cancellation)?;
675        let address = self.capabilities.symbols().env_list_declarations;
676        // SAFETY: per the SessionSymbols::resolve invariant; signature
677        // is `Environment -> IO (Array Name)`.
678        let list: LeanExported<'lean, '_, (Obj<'lean>,), LeanIo<Vec<Obj<'lean>>>> =
679            unsafe { LeanExported::from_function_address(self.runtime(), address) };
680        let t = Instant::now();
681        let raw = list.call(self.environment.clone());
682        self.record_call(0, t.elapsed());
683        raw?.into_iter().map(LeanName::try_from_lean).collect()
684    }
685
686    /// Declaration names matching `filter`.
687    ///
688    /// Filtering runs inside Lean while traversing the environment
689    /// constants table, so Rust only allocates handles for names the
690    /// caller asked to keep.
691    ///
692    /// # Errors
693    ///
694    /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
695    /// already cancelled before dispatch. Returns
696    /// [`lean_rs::LeanError::LeanException`] if the Lean-side query
697    /// raises.
698    pub fn list_declarations_filtered(
699        &mut self,
700        filter: &LeanDeclarationFilter,
701        cancellation: Option<&LeanCancellationToken>,
702        progress: Option<&dyn LeanProgressSink>,
703    ) -> LeanResult<Vec<LeanName<'lean>>> {
704        let _span = tracing::debug_span!(
705            target: "lean_rs",
706            "lean_rs.host.session.list_declarations_filtered",
707            include_private = filter.include_private,
708            include_generated = filter.include_generated,
709            include_internal = filter.include_internal,
710        )
711        .entered();
712        check_cancellation(cancellation)?;
713        let raw = if let Some(sink) = progress {
714            let bridge = ProgressBridge::new(sink, "list_declarations_filtered", None)?;
715            let (handle, trampoline) = bridge.abi_parts();
716            let address = self.capabilities.symbols().env_list_declarations_filtered_progress;
717            // SAFETY: per the SessionSymbols::resolve invariant; signature
718            // is `(Environment, DeclarationFilter, USize, USize) ->
719            // IO (Except UInt8 (Array Name))`.
720            let list: LeanExported<'lean, '_, (Obj<'lean>, LeanDeclarationFilter, usize, usize), LeanIo<Obj<'lean>>> =
721                unsafe { LeanExported::from_function_address(self.runtime(), address) };
722            let t = Instant::now();
723            let result = list.call(self.environment.clone(), *filter, handle, trampoline);
724            self.record_call(0, t.elapsed());
725            bridge.decode::<Vec<Obj<'lean>>>(result?)?
726        } else {
727            let address = self.capabilities.symbols().env_list_declarations_filtered;
728            // SAFETY: per the SessionSymbols::resolve invariant; signature
729            // is `(Environment, DeclarationFilter) -> IO (Array Name)`.
730            let list: LeanExported<'lean, '_, (Obj<'lean>, LeanDeclarationFilter), LeanIo<Vec<Obj<'lean>>>> =
731                unsafe { LeanExported::from_function_address(self.runtime(), address) };
732            let t = Instant::now();
733            let result = list.call(self.environment.clone(), *filter);
734            self.record_call(0, t.elapsed());
735            result?
736        };
737        raw.into_iter().map(LeanName::try_from_lean).collect()
738    }
739
740    /// Source range Lean recorded for `name`.
741    ///
742    /// Returns `Ok(None)` when the name is absent or Lean has no
743    /// declaration range for it. That is normal for synthetic,
744    /// runtime-created, and some compiler-generated declarations.
745    ///
746    /// # Errors
747    ///
748    /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
749    /// already cancelled before dispatch. Returns
750    /// [`lean_rs::LeanError::LeanException`] if the Lean-side query
751    /// raises.
752    pub fn declaration_source_range(
753        &mut self,
754        name: &str,
755        cancellation: Option<&LeanCancellationToken>,
756    ) -> LeanResult<Option<LeanSourceRange>> {
757        let _span = tracing::debug_span!(
758            target: "lean_rs",
759            "lean_rs.host.session.declaration_source_range",
760            name = name,
761        )
762        .entered();
763        check_cancellation(cancellation)?;
764        let name_handle = self.make_name(name, cancellation)?;
765        check_cancellation(cancellation)?;
766        let source_roots = self
767            .capabilities
768            .host()
769            .project()
770            .source_roots()?
771            .into_iter()
772            .map(|path| path.to_string_lossy().into_owned())
773            .collect::<Vec<_>>();
774        check_cancellation(cancellation)?;
775        let address = self.capabilities.symbols().env_declaration_source_range;
776        // SAFETY: per the SessionSymbols::resolve invariant; signature
777        // is `(Environment, Name, Array String) -> IO (Option SourceRange)`.
778        let query: LeanExported<
779            'lean,
780            '_,
781            (Obj<'lean>, LeanName<'lean>, Vec<String>),
782            LeanIo<Option<LeanSourceRange>>,
783        > = unsafe { LeanExported::from_function_address(self.runtime(), address) };
784        let t = Instant::now();
785        let result = query.call(self.environment.clone(), name_handle, source_roots);
786        self.record_call(0, t.elapsed());
787        result
788    }
789
790    /// The declared type of `name`, as an opaque [`LeanExpr`] handle.
791    ///
792    /// Returns `Ok(None)` if the name is not present in the environment.
793    ///
794    /// # Errors
795    ///
796    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side query
797    /// raises.
798    pub fn declaration_type(
799        &mut self,
800        name: &str,
801        cancellation: Option<&LeanCancellationToken>,
802    ) -> LeanResult<Option<LeanExpr<'lean>>> {
803        let _span = tracing::debug_span!(
804            target: "lean_rs",
805            "lean_rs.host.session.declaration_type",
806            name = name,
807        )
808        .entered();
809        check_cancellation(cancellation)?;
810        let name_handle = self.make_name(name, cancellation)?;
811        check_cancellation(cancellation)?;
812        let address = self.capabilities.symbols().env_declaration_type;
813        // SAFETY: per the SessionSymbols::resolve invariant; signature
814        // is `(Environment, Name) -> IO (Option Expr)`.
815        let query: LeanExported<'lean, '_, (Obj<'lean>, LeanName<'lean>), LeanIo<Option<LeanExpr<'lean>>>> =
816            unsafe { LeanExported::from_function_address(self.runtime(), address) };
817        let t = Instant::now();
818        let result = query.call(self.environment.clone(), name_handle);
819        self.record_call(0, t.elapsed());
820        result
821    }
822
823    /// The declared types of `names`, preserving input order.
824    ///
825    /// Returns `None` in each slot whose name is not present in the
826    /// environment. With `cancellation = None`, the whole batch crosses
827    /// the FFI boundary once and Lean converts the input strings to
828    /// names internally. With `Some(token)`, this loops through
829    /// [`Self::declaration_type`] so cancellation can be observed
830    /// between items; partial results are discarded when cancellation
831    /// fires.
832    ///
833    /// # Errors
834    ///
835    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side
836    /// bulk shim raises through `IO`.
837    pub fn declaration_type_bulk(
838        &mut self,
839        names: &[&str],
840        cancellation: Option<&LeanCancellationToken>,
841        progress: Option<&dyn LeanProgressSink>,
842    ) -> LeanResult<Vec<Option<LeanExpr<'lean>>>> {
843        let _span = tracing::debug_span!(
844            target: "lean_rs",
845            "lean_rs.host.session.declaration_type_bulk",
846            batch_size = names.len(),
847        )
848        .entered();
849        if names.is_empty() {
850            return Ok(Vec::new());
851        }
852        check_cancellation(cancellation)?;
853        if cancellation.is_some() {
854            let started = Instant::now();
855            let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
856            let mut out = Vec::with_capacity(names.len());
857            for (idx, name) in names.iter().enumerate() {
858                check_cancellation(cancellation)?;
859                out.push(self.declaration_type(name, cancellation)?);
860                report_progress(
861                    progress,
862                    "declaration_type_bulk",
863                    u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
864                    total,
865                    started,
866                )?;
867            }
868            return Ok(out);
869        }
870        if progress.is_none()
871            && let Some(name) = Self::all_equal_name(names)
872        {
873            let names_owned = vec![name.to_owned()];
874            let address = self.capabilities.symbols().env_declaration_type_bulk;
875            // SAFETY: per the SessionSymbols::resolve invariant; signature
876            // is `(Environment, Array String) -> IO (Array (Option Expr))`.
877            let query: LeanExported<'lean, '_, (Obj<'lean>, Vec<String>), LeanIo<Vec<Option<LeanExpr<'lean>>>>> =
878                unsafe { LeanExported::from_function_address(self.runtime(), address) };
879            let t = Instant::now();
880            let mut result = query.call(self.environment.clone(), names_owned)?;
881            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
882            self.record_call(batch_len, t.elapsed());
883            let value = result.pop().unwrap_or(None);
884            return Ok(vec![value; names.len()]);
885        }
886        let names_owned: Vec<String> = names.iter().map(|&name| name.to_owned()).collect();
887        if let Some(sink) = progress {
888            let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
889            let bridge = ProgressBridge::new(sink, "declaration_type_bulk", total)?;
890            let (handle, trampoline) = bridge.abi_parts();
891            let address = self.capabilities.symbols().env_declaration_type_bulk_progress;
892            // SAFETY: per the SessionSymbols::resolve invariant; signature
893            // is `(Environment, Array String, USize, USize) ->
894            // IO (Except UInt8 (Array (Option Expr)))`.
895            let query: LeanExported<'lean, '_, (Obj<'lean>, Vec<String>, usize, usize), LeanIo<Obj<'lean>>> =
896                unsafe { LeanExported::from_function_address(self.runtime(), address) };
897            let t = Instant::now();
898            let result = query.call(self.environment.clone(), names_owned, handle, trampoline);
899            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
900            self.record_call(batch_len, t.elapsed());
901            bridge.decode(result?)
902        } else {
903            let address = self.capabilities.symbols().env_declaration_type_bulk;
904            // SAFETY: per the SessionSymbols::resolve invariant; signature
905            // is `(Environment, Array String) -> IO (Array (Option Expr))`.
906            let query: LeanExported<'lean, '_, (Obj<'lean>, Vec<String>), LeanIo<Vec<Option<LeanExpr<'lean>>>>> =
907                unsafe { LeanExported::from_function_address(self.runtime(), address) };
908            let t = Instant::now();
909            let result = query.call(self.environment.clone(), names_owned);
910            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
911            self.record_call(batch_len, t.elapsed());
912            result
913        }
914    }
915
916    /// The kind of `name` as a Lean-rendered string
917    /// (`"axiom"`, `"definition"`, `"theorem"`, `"opaque"`, `"quot"`,
918    /// `"inductive"`, `"constructor"`, `"recursor"`), or `"missing"`
919    /// if `name` is not in the environment.
920    ///
921    /// # Errors
922    ///
923    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side query
924    /// raises.
925    pub fn declaration_kind(&mut self, name: &str, cancellation: Option<&LeanCancellationToken>) -> LeanResult<String> {
926        let _span = tracing::debug_span!(
927            target: "lean_rs",
928            "lean_rs.host.session.declaration_kind",
929            name = name,
930        )
931        .entered();
932        check_cancellation(cancellation)?;
933        let name_handle = self.make_name(name, cancellation)?;
934        check_cancellation(cancellation)?;
935        let address = self.capabilities.symbols().env_declaration_kind;
936        // SAFETY: per the SessionSymbols::resolve invariant; signature
937        // is `(Environment, Name) -> IO String`.
938        let query: LeanExported<'lean, '_, (Obj<'lean>, LeanName<'lean>), LeanIo<String>> =
939            unsafe { LeanExported::from_function_address(self.runtime(), address) };
940        let t = Instant::now();
941        let result = query.call(self.environment.clone(), name_handle);
942        self.record_call(0, t.elapsed());
943        result
944    }
945
946    /// The declaration kinds of `names`, preserving input order.
947    ///
948    /// Each output slot is the same string that [`Self::declaration_kind`]
949    /// would return for the corresponding input, including `"missing"`
950    /// for absent declarations. With `cancellation = None`, this is one
951    /// Lean-side bulk dispatch over an `Array String`; with
952    /// `Some(token)`, this loops through the singular path so the token
953    /// can be checked between items.
954    ///
955    /// # Errors
956    ///
957    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side
958    /// bulk shim raises through `IO`.
959    pub fn declaration_kind_bulk(
960        &mut self,
961        names: &[&str],
962        cancellation: Option<&LeanCancellationToken>,
963        progress: Option<&dyn LeanProgressSink>,
964    ) -> LeanResult<Vec<String>> {
965        let _span = tracing::debug_span!(
966            target: "lean_rs",
967            "lean_rs.host.session.declaration_kind_bulk",
968            batch_size = names.len(),
969        )
970        .entered();
971        if names.is_empty() {
972            return Ok(Vec::new());
973        }
974        check_cancellation(cancellation)?;
975        if cancellation.is_some() {
976            let started = Instant::now();
977            let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
978            let mut out = Vec::with_capacity(names.len());
979            for (idx, name) in names.iter().enumerate() {
980                check_cancellation(cancellation)?;
981                out.push(self.declaration_kind(name, cancellation)?);
982                report_progress(
983                    progress,
984                    "declaration_kind_bulk",
985                    u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
986                    total,
987                    started,
988                )?;
989            }
990            return Ok(out);
991        }
992        if progress.is_none()
993            && let Some(name) = Self::all_equal_name(names)
994        {
995            let names_owned = vec![name.to_owned()];
996            let address = self.capabilities.symbols().env_declaration_kind_bulk;
997            // SAFETY: per the SessionSymbols::resolve invariant; signature
998            // is `(Environment, Array String) -> IO (Array String)`.
999            let query: LeanExported<'lean, '_, (Obj<'lean>, Vec<String>), LeanIo<Vec<Obj<'lean>>>> =
1000                unsafe { LeanExported::from_function_address(self.runtime(), address) };
1001            let t = Instant::now();
1002            let mut result = Self::decode_strings_cached(query.call(self.environment.clone(), names_owned)?)?;
1003            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1004            self.record_call(batch_len, t.elapsed());
1005            let value = result.pop().unwrap_or_default();
1006            return Ok(vec![value; names.len()]);
1007        }
1008        let names_owned: Vec<String> = names.iter().map(|&name| name.to_owned()).collect();
1009        if let Some(sink) = progress {
1010            let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1011            let bridge = ProgressBridge::new(sink, "declaration_kind_bulk", total)?;
1012            let (handle, trampoline) = bridge.abi_parts();
1013            let address = self.capabilities.symbols().env_declaration_kind_bulk_progress;
1014            // SAFETY: per the SessionSymbols::resolve invariant; signature
1015            // is `(Environment, Array String, USize, USize) ->
1016            // IO (Except UInt8 (Array String))`.
1017            let query: LeanExported<'lean, '_, (Obj<'lean>, Vec<String>, usize, usize), LeanIo<Obj<'lean>>> =
1018                unsafe { LeanExported::from_function_address(self.runtime(), address) };
1019            let t = Instant::now();
1020            let result = query.call(self.environment.clone(), names_owned, handle, trampoline);
1021            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1022            self.record_call(batch_len, t.elapsed());
1023            let raw = bridge.decode::<Vec<Obj<'lean>>>(result?)?;
1024            Self::decode_strings_cached(raw)
1025        } else {
1026            let address = self.capabilities.symbols().env_declaration_kind_bulk;
1027            // SAFETY: per the SessionSymbols::resolve invariant; signature
1028            // is `(Environment, Array String) -> IO (Array String)`.
1029            let query: LeanExported<'lean, '_, (Obj<'lean>, Vec<String>), LeanIo<Vec<Obj<'lean>>>> =
1030                unsafe { LeanExported::from_function_address(self.runtime(), address) };
1031            let t = Instant::now();
1032            let result = query.call(self.environment.clone(), names_owned);
1033            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1034            self.record_call(batch_len, t.elapsed());
1035            Self::decode_strings_cached(result?)
1036        }
1037    }
1038
1039    /// The Lean-rendered display string of `name`. Round-trips a name
1040    /// through the capability's `Name.toString` shim so callers see the
1041    /// same canonical form Lean would log.
1042    ///
1043    /// Diagnostic only — not a semantic key. Use
1044    /// [`LeanSession::query_declaration`] + a typed handle when
1045    /// equality matters.
1046    ///
1047    /// # Errors
1048    ///
1049    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side query
1050    /// raises.
1051    pub fn declaration_name(&mut self, name: &str, cancellation: Option<&LeanCancellationToken>) -> LeanResult<String> {
1052        let _span = tracing::debug_span!(
1053            target: "lean_rs",
1054            "lean_rs.host.session.declaration_name",
1055            name = name,
1056        )
1057        .entered();
1058        check_cancellation(cancellation)?;
1059        let name_handle = self.make_name(name, cancellation)?;
1060        check_cancellation(cancellation)?;
1061        let address = self.capabilities.symbols().env_declaration_name;
1062        // SAFETY: per the SessionSymbols::resolve invariant; signature
1063        // is `(Environment, Name) -> IO String`.
1064        let query: LeanExported<'lean, '_, (Obj<'lean>, LeanName<'lean>), LeanIo<String>> =
1065            unsafe { LeanExported::from_function_address(self.runtime(), address) };
1066        let t = Instant::now();
1067        let result = query.call(self.environment.clone(), name_handle);
1068        self.record_call(0, t.elapsed());
1069        result
1070    }
1071
1072    /// Lean-rendered display strings for `names`, preserving input
1073    /// order.
1074    ///
1075    /// This is diagnostic text, not a semantic key. Missing
1076    /// declarations are not an error because the singular
1077    /// [`Self::declaration_name`] path also only round-trips the input
1078    /// name through Lean's `Name.toString` renderer.
1079    ///
1080    /// With `cancellation = None`, this is one Lean-side bulk dispatch
1081    /// over an `Array String`; with `Some(token)`, this loops through
1082    /// the singular path so the token can be checked between items.
1083    ///
1084    /// # Errors
1085    ///
1086    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side
1087    /// bulk shim raises through `IO`.
1088    pub fn declaration_name_bulk(
1089        &mut self,
1090        names: &[&str],
1091        cancellation: Option<&LeanCancellationToken>,
1092        progress: Option<&dyn LeanProgressSink>,
1093    ) -> LeanResult<Vec<String>> {
1094        let _span = tracing::debug_span!(
1095            target: "lean_rs",
1096            "lean_rs.host.session.declaration_name_bulk",
1097            batch_size = names.len(),
1098        )
1099        .entered();
1100        if names.is_empty() {
1101            return Ok(Vec::new());
1102        }
1103        check_cancellation(cancellation)?;
1104        if cancellation.is_some() {
1105            let started = Instant::now();
1106            let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1107            let mut out = Vec::with_capacity(names.len());
1108            for (idx, name) in names.iter().enumerate() {
1109                check_cancellation(cancellation)?;
1110                out.push(self.declaration_name(name, cancellation)?);
1111                report_progress(
1112                    progress,
1113                    "declaration_name_bulk",
1114                    u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
1115                    total,
1116                    started,
1117                )?;
1118            }
1119            return Ok(out);
1120        }
1121        if progress.is_none()
1122            && let Some(name) = Self::all_equal_name(names)
1123        {
1124            let names_owned = vec![name.to_owned()];
1125            let address = self.capabilities.symbols().env_declaration_name_bulk;
1126            // SAFETY: per the SessionSymbols::resolve invariant; signature
1127            // is `(Environment, Array String) -> IO (Array String)`.
1128            let query: LeanExported<'lean, '_, (Obj<'lean>, Vec<String>), LeanIo<Vec<Obj<'lean>>>> =
1129                unsafe { LeanExported::from_function_address(self.runtime(), address) };
1130            let t = Instant::now();
1131            let mut result = Self::decode_strings_cached(query.call(self.environment.clone(), names_owned)?)?;
1132            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1133            self.record_call(batch_len, t.elapsed());
1134            let value = result.pop().unwrap_or_default();
1135            return Ok(vec![value; names.len()]);
1136        }
1137        let names_owned: Vec<String> = names.iter().map(|&name| name.to_owned()).collect();
1138        if let Some(sink) = progress {
1139            let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1140            let bridge = ProgressBridge::new(sink, "declaration_name_bulk", total)?;
1141            let (handle, trampoline) = bridge.abi_parts();
1142            let address = self.capabilities.symbols().env_declaration_name_bulk_progress;
1143            // SAFETY: per the SessionSymbols::resolve invariant; signature
1144            // is `(Environment, Array String, USize, USize) ->
1145            // IO (Except UInt8 (Array String))`.
1146            let query: LeanExported<'lean, '_, (Obj<'lean>, Vec<String>, usize, usize), LeanIo<Obj<'lean>>> =
1147                unsafe { LeanExported::from_function_address(self.runtime(), address) };
1148            let t = Instant::now();
1149            let result = query.call(self.environment.clone(), names_owned, handle, trampoline);
1150            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1151            self.record_call(batch_len, t.elapsed());
1152            let raw = bridge.decode::<Vec<Obj<'lean>>>(result?)?;
1153            Self::decode_strings_cached(raw)
1154        } else {
1155            let address = self.capabilities.symbols().env_declaration_name_bulk;
1156            // SAFETY: per the SessionSymbols::resolve invariant; signature
1157            // is `(Environment, Array String) -> IO (Array String)`.
1158            let query: LeanExported<'lean, '_, (Obj<'lean>, Vec<String>), LeanIo<Vec<Obj<'lean>>>> =
1159                unsafe { LeanExported::from_function_address(self.runtime(), address) };
1160            let t = Instant::now();
1161            let result = query.call(self.environment.clone(), names_owned);
1162            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1163            self.record_call(batch_len, t.elapsed());
1164            Self::decode_strings_cached(result?)
1165        }
1166    }
1167
1168    /// Render an opaque [`LeanName`] handle as its dotted-string form,
1169    /// routed through the capability's `Name.toString` shim.
1170    ///
1171    /// This is the supported way to turn a `LeanName` (e.g. an element
1172    /// of [`Self::list_declarations_filtered`]'s result) into Rust text.
1173    /// The output is diagnostic — not a semantic key — and equality on
1174    /// the underlying `Lean.Name` still lives in Lean.
1175    ///
1176    /// # Errors
1177    ///
1178    /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
1179    /// already cancelled before dispatch.
1180    pub fn name_to_string(
1181        &mut self,
1182        name: &LeanName<'lean>,
1183        cancellation: Option<&LeanCancellationToken>,
1184    ) -> LeanResult<String> {
1185        let _span = tracing::debug_span!(target: "lean_rs", "lean_rs.host.session.name_to_string").entered();
1186        check_cancellation(cancellation)?;
1187        let address = self.capabilities.symbols().name_to_string;
1188        // SAFETY: per the SessionSymbols::resolve invariant; signature
1189        // is `Name -> String` (pure, not IO).
1190        let render: LeanExported<'lean, '_, (LeanName<'lean>,), String> =
1191            unsafe { LeanExported::from_function_address(self.runtime(), address) };
1192        let t = Instant::now();
1193        let result = render.call(name.clone());
1194        self.record_call(0, t.elapsed());
1195        result
1196    }
1197
1198    /// Render `names` as dotted-string forms, preserving input order.
1199    ///
1200    /// Implemented as a per-item loop over [`Self::name_to_string`] in
1201    /// v1: cancellation is checked between items, progress is reported
1202    /// after each. The Lean shim is pure and short, so the per-item FFI
1203    /// overhead is acceptable; a bulk shim is a future optimisation if
1204    /// profiling shows it matters.
1205    ///
1206    /// # Errors
1207    ///
1208    /// Returns [`lean_rs::LeanError::Cancelled`] between items if the
1209    /// token is tripped during the walk.
1210    pub fn name_to_string_bulk(
1211        &mut self,
1212        names: &[LeanName<'lean>],
1213        cancellation: Option<&LeanCancellationToken>,
1214        progress: Option<&dyn LeanProgressSink>,
1215    ) -> LeanResult<Vec<String>> {
1216        let _span = tracing::debug_span!(
1217            target: "lean_rs",
1218            "lean_rs.host.session.name_to_string_bulk",
1219            batch_size = names.len(),
1220        )
1221        .entered();
1222        if names.is_empty() {
1223            return Ok(Vec::new());
1224        }
1225        check_cancellation(cancellation)?;
1226        let started = Instant::now();
1227        let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1228        let mut out = Vec::with_capacity(names.len());
1229        for (idx, name) in names.iter().enumerate() {
1230            check_cancellation(cancellation)?;
1231            out.push(self.name_to_string(name, cancellation)?);
1232            report_progress(
1233                progress,
1234                "name_to_string_bulk",
1235                u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
1236                total,
1237                started,
1238            )?;
1239        }
1240        Ok(out)
1241    }
1242
1243    /// Enumerate the imported environment's declaration names and render
1244    /// each as a dotted string. Convenience over
1245    /// [`Self::list_declarations_filtered`] + [`Self::name_to_string_bulk`]
1246    /// for the common case where the consumer only needs strings.
1247    ///
1248    /// Two FFI hops (list + per-name render) and one heap allocation
1249    /// per name. For batches under a few thousand this is fine; for
1250    /// six-figure walks consider the lower-level pair so the listing
1251    /// pass and the rendering pass can be cancelled or chunked
1252    /// independently.
1253    ///
1254    /// # Errors
1255    ///
1256    /// Forwards errors from [`Self::list_declarations_filtered`] and
1257    /// [`Self::name_to_string_bulk`].
1258    pub fn list_declarations_strings(
1259        &mut self,
1260        filter: &LeanDeclarationFilter,
1261        cancellation: Option<&LeanCancellationToken>,
1262        progress: Option<&dyn LeanProgressSink>,
1263    ) -> LeanResult<Vec<String>> {
1264        let _span = tracing::debug_span!(target: "lean_rs", "lean_rs.host.session.list_declarations_strings").entered();
1265        let names = self.list_declarations_filtered(filter, cancellation, None)?;
1266        self.name_to_string_bulk(&names, cancellation, progress)
1267    }
1268
1269    /// Render `expr` via `Expr.toString` — the cheap, deterministic
1270    /// projection.
1271    ///
1272    /// Walks the syntax tree directly: no `MetaM`, no notation lookup,
1273    /// no binder pretty-printing. The result is a legible-but-ugly
1274    /// dump suitable for indexing, logging, and search keys. For the
1275    /// form a Lean user reads, use the optional
1276    /// [`crate::host::meta::pp_expr`] service through
1277    /// [`Self::run_meta`] instead — it pays for elaboration context to
1278    /// get notation and unfolding right but can time out under a tight
1279    /// heartbeat budget.
1280    ///
1281    /// # Errors
1282    ///
1283    /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
1284    /// already cancelled before dispatch.
1285    pub fn expr_to_string_raw(
1286        &mut self,
1287        expr: &LeanExpr<'lean>,
1288        cancellation: Option<&LeanCancellationToken>,
1289    ) -> LeanResult<String> {
1290        let _span = tracing::debug_span!(target: "lean_rs", "lean_rs.host.session.expr_to_string_raw").entered();
1291        check_cancellation(cancellation)?;
1292        let address = self.capabilities.symbols().env_expr_to_string_raw;
1293        // SAFETY: per the SessionSymbols::resolve invariant; signature
1294        // is `Expr -> String` (pure, not IO).
1295        let render: LeanExported<'lean, '_, (LeanExpr<'lean>,), String> =
1296            unsafe { LeanExported::from_function_address(self.runtime(), address) };
1297        let t = Instant::now();
1298        let result = render.call(expr.clone());
1299        self.record_call(0, t.elapsed());
1300        result
1301    }
1302
1303    /// Parse, elaborate, and project a Lean source string into its
1304    /// `Elab.InfoTree`.
1305    ///
1306    /// Drives Lean's `IO.processCommands` pipeline with info collection
1307    /// enabled, walks every captured info tree, and projects each
1308    /// `TermInfo` / `TacticInfo` / `CommandInfo` / identifier reference
1309    /// into a [`ProcessedFile`] value. The projection is the FFI
1310    /// boundary — raw `InfoTree` does not cross, because it carries
1311    /// metavariable contexts that cannot be revived outside the
1312    /// session that produced them.
1313    ///
1314    /// Goal text in `TacticInfoNode::goals_before` /
1315    /// `TacticInfoNode::goals_after` is pre-rendered through Lean's
1316    /// `Meta.ppGoal` inside the elaboration context; the cumulative
1317    /// goal-byte sum is bounded by [`LeanElabOptions::diagnostic_byte_limit`].
1318    ///
1319    /// The shim is optional. When the loaded capability dylib does not
1320    /// export `lean_rs_host_process_with_info_tree`, the method returns
1321    /// [`ProcessFileOutcome::Unsupported`] without an FFI call —
1322    /// matching the meta-service degradation pattern. There is no
1323    /// separate timeout arm because `IO.processCommands` catches
1324    /// per-command exceptions and attaches them to the message log;
1325    /// inspect [`ProcessedFile::diagnostics`] to detect heartbeat
1326    /// exhaustion.
1327    ///
1328    /// # Errors
1329    ///
1330    /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
1331    /// already cancelled before dispatch. Returns
1332    /// [`lean_rs::LeanError::LeanException`] if the Lean-side shim
1333    /// raises through `IO`. Returns [`lean_rs::LeanError::Host`] with
1334    /// stage [`HostStage::Conversion`] if the Lean return value does
1335    /// not decode into [`ProcessedFile`].
1336    pub fn process_with_info_tree(
1337        &mut self,
1338        source: &str,
1339        options: &LeanElabOptions,
1340        cancellation: Option<&LeanCancellationToken>,
1341    ) -> LeanResult<ProcessFileOutcome> {
1342        let _span = tracing::debug_span!(
1343            target: "lean_rs",
1344            "lean_rs.host.session.process_with_info_tree",
1345            source_len = source.len(),
1346            heartbeats = options.heartbeats(),
1347            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1348        )
1349        .entered();
1350        check_cancellation(cancellation)?;
1351        let Some(address) = self.capabilities.symbols().process_with_info_tree else {
1352            return Ok(ProcessFileOutcome::Unsupported);
1353        };
1354        // SAFETY: per the SessionSymbols::resolve invariant; signature
1355        // is `(Environment, String, String, String, UInt64, USize) ->
1356        // IO ProcessedFile`.
1357        let call: LeanExported<'lean, '_, (Obj<'lean>, &str, &str, &str, u64, usize), LeanIo<ProcessedFile>> =
1358            unsafe { LeanExported::from_function_address(self.runtime(), address) };
1359        let t = Instant::now();
1360        let result = call.call(
1361            self.environment.clone(),
1362            source,
1363            options.namespace_context_str(),
1364            options.file_label_str(),
1365            options.heartbeats(),
1366            options.diagnostic_byte_limit_usize(),
1367        );
1368        self.record_call(0, t.elapsed());
1369        Ok(ProcessFileOutcome::Processed(result?))
1370    }
1371
1372    /// Parse a Lean source file's header, then resume elaboration of
1373    /// the body against the session's open env, returning the
1374    /// info-tree projection in the original file's coordinate system.
1375    ///
1376    /// Unlike [`Self::process_with_info_tree`] (which is right for
1377    /// header-less snippets), this method calls
1378    /// `Lean.Parser.parseHeader` first and resumes `IO.processCommands`
1379    /// from the parser state the header parser produced. The shared
1380    /// `InputContext.fileMap` covers the whole source, so positions in
1381    /// the returned [`ProcessedFile`] use the original file's line and
1382    /// column numbers with no Rust-side offset arithmetic.
1383    ///
1384    /// Three real outcomes plus an `Unsupported` degradation arm:
1385    ///
1386    /// - [`ProcessModuleOutcome::Ok`] — header parsed; every
1387    ///   user-written import is present in the session's open env; the
1388    ///   body was processed. `imports` lists the user-written modules
1389    ///   (Lean's auto-inserted `Init` is filtered out by the shim).
1390    /// - [`ProcessModuleOutcome::MissingImports`] — header
1391    ///   parsed but some imports name modules the open env lacks. The
1392    ///   body still elaborated against whatever the env carries; the
1393    ///   projection is populated and `missing` lists the absent
1394    ///   modules. Soft failure — callers typically surface it as a
1395    ///   warning rather than as an error.
1396    /// - [`ProcessModuleOutcome::HeaderParseFailed`] — the
1397    ///   header itself did not parse (e.g. a malformed `import` line).
1398    ///   `IO.processCommands` was not run; only the header diagnostics
1399    ///   are returned.
1400    /// - [`ProcessModuleOutcome::Unsupported`] — the loaded
1401    ///   capability dylib does not export
1402    ///   `lean_rs_host_process_module_with_info_tree`. No FFI call was
1403    ///   made.
1404    ///
1405    /// As with [`Self::process_with_info_tree`], heartbeat exhaustion
1406    /// during a single command's elaboration surfaces as an
1407    /// error-severity entry in
1408    /// [`ProcessedFile::diagnostics`](crate::host::process::ProcessedFile)
1409    /// rather than as a separate wire arm.
1410    ///
1411    /// # Errors
1412    ///
1413    /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
1414    /// already cancelled before dispatch. Returns
1415    /// [`lean_rs::LeanError::LeanException`] if the Lean-side shim
1416    /// raises through `IO`. Returns [`lean_rs::LeanError::Host`] with
1417    /// stage [`HostStage::Conversion`] if the Lean return value does
1418    /// not decode into [`ProcessModuleOutcome`].
1419    pub fn process_module_with_info_tree(
1420        &mut self,
1421        source: &str,
1422        options: &LeanElabOptions,
1423        cancellation: Option<&LeanCancellationToken>,
1424    ) -> LeanResult<ProcessModuleOutcome> {
1425        let _span = tracing::debug_span!(
1426            target: "lean_rs",
1427            "lean_rs.host.session.process_module_with_info_tree",
1428            source_len = source.len(),
1429            heartbeats = options.heartbeats(),
1430            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1431        )
1432        .entered();
1433        check_cancellation(cancellation)?;
1434        let Some(address) = self.capabilities.symbols().process_module_with_info_tree else {
1435            return Ok(ProcessModuleOutcome::Unsupported);
1436        };
1437        // SAFETY: per the SessionSymbols::resolve invariant; signature
1438        // is `(Environment, String, String, String, UInt64, USize) ->
1439        // IO ProcessModuleOutcome`.
1440        let call: LeanExported<'lean, '_, (Obj<'lean>, &str, &str, &str, u64, usize), LeanIo<ProcessModuleOutcome>> =
1441            unsafe { LeanExported::from_function_address(self.runtime(), address) };
1442        let t = Instant::now();
1443        let result = call.call(
1444            self.environment.clone(),
1445            source,
1446            options.namespace_context_str(),
1447            options.file_label_str(),
1448            options.heartbeats(),
1449            options.diagnostic_byte_limit_usize(),
1450        );
1451        self.record_call(0, t.elapsed());
1452        result
1453    }
1454
1455    /// Parse and elaborate a single Lean term against the imported
1456    /// environment, optionally against an expected type.
1457    ///
1458    /// The boundary is explicit: Rust supplies the source text, module
1459    /// context, and bounded options; Lean parses, elaborates, and
1460    /// returns either an opaque [`LeanExpr`] handle or a structured
1461    /// [`LeanElabFailure`] carrying typed diagnostics. Rust does not
1462    /// inspect elaborator internals or proof terms to decide
1463    /// correctness.
1464    ///
1465    /// The outer [`LeanResult`] surfaces host-stack failures (a Lean
1466    /// `IO`-level exception from the shim itself, a malformed Lean
1467    /// return value); the inner `Result` distinguishes successful
1468    /// elaboration from parse / type / kernel-stage failures the
1469    /// elaborator reports through its `MessageLog`. Both error paths
1470    /// propagate the [`LeanElabOptions::diagnostic_byte_limit`] bound
1471    /// structurally.
1472    ///
1473    /// # Errors
1474    ///
1475    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side shim raises
1476    /// through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
1477    /// [`HostStage::Conversion`] if the Lean return value does not
1478    /// decode into [`LeanElabFailure`] / [`LeanExpr`].
1479    pub fn elaborate(
1480        &mut self,
1481        source: &str,
1482        expected_type: Option<&LeanExpr<'lean>>,
1483        options: &LeanElabOptions,
1484        cancellation: Option<&LeanCancellationToken>,
1485    ) -> LeanResult<Result<LeanExpr<'lean>, LeanElabFailure>> {
1486        let _span = tracing::debug_span!(
1487            target: "lean_rs",
1488            "lean_rs.host.session.elaborate",
1489            source_len = source.len(),
1490            heartbeats = options.heartbeats(),
1491            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1492        )
1493        .entered();
1494        check_cancellation(cancellation)?;
1495        let address = self.capabilities.symbols().elaborate;
1496        // SAFETY: per the SessionSymbols::resolve invariant; signature
1497        // is `(Environment, String, Option Expr, String, String,
1498        // UInt64, USize) -> IO (Except ElabFailure Expr)`.
1499        let call: LeanExported<
1500            'lean,
1501            '_,
1502            (Obj<'lean>, &str, Option<LeanExpr<'lean>>, &str, &str, u64, usize),
1503            LeanIo<Result<LeanExpr<'lean>, LeanElabFailure>>,
1504        > = unsafe { LeanExported::from_function_address(self.runtime(), address) };
1505        let t = Instant::now();
1506        let result = call.call(
1507            self.environment.clone(),
1508            source,
1509            expected_type.cloned(),
1510            options.namespace_context_str(),
1511            options.file_label_str(),
1512            options.heartbeats(),
1513            options.diagnostic_byte_limit_usize(),
1514        );
1515        self.record_call(0, t.elapsed());
1516        result
1517    }
1518
1519    /// Parse, elaborate, and kernel-check a Lean declaration source
1520    /// (typically a `theorem` or `def`), returning a typed outcome
1521    /// that classifies the result and carries either the produced
1522    /// [`crate::LeanEvidence`] handle or the diagnostics the elaborator and
1523    /// kernel emitted.
1524    ///
1525    /// The boundary is explicit (mirrors [`Self::elaborate`]): Rust
1526    /// supplies source + options; Lean parses, elaborates, runs
1527    /// `addDecl` (which kernel-checks), and classifies the outcome.
1528    /// Rust never inspects the produced proof term or declaration
1529    /// internals.
1530    ///
1531    /// # Errors
1532    ///
1533    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side shim
1534    /// raises through `IO` (an unexpected internal failure that is not
1535    /// itself a rejection / unavailable diagnostic). Returns
1536    /// [`lean_rs::LeanError::Host`] with stage [`HostStage::Conversion`] if the
1537    /// Lean return value does not decode into [`LeanKernelOutcome`].
1538    pub fn kernel_check(
1539        &mut self,
1540        source: &str,
1541        options: &LeanElabOptions,
1542        cancellation: Option<&LeanCancellationToken>,
1543        progress: Option<&dyn LeanProgressSink>,
1544    ) -> LeanResult<LeanKernelOutcome<'lean>> {
1545        let _span = tracing::debug_span!(
1546            target: "lean_rs",
1547            "lean_rs.host.session.kernel_check",
1548            source_len = source.len(),
1549            heartbeats = options.heartbeats(),
1550            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1551        )
1552        .entered();
1553        check_cancellation(cancellation)?;
1554        if let Some(sink) = progress {
1555            let bridge = ProgressBridge::new(sink, "kernel_check", Some(1))?;
1556            let (handle, trampoline) = bridge.abi_parts();
1557            let address = self.capabilities.symbols().kernel_check_progress;
1558            // SAFETY: per the SessionSymbols::resolve invariant; signature
1559            // is `(Environment, String, String, String, UInt64, USize,
1560            // USize, USize) -> IO (Except UInt8 KernelOutcome)`.
1561            let call: LeanExported<
1562                'lean,
1563                '_,
1564                (Obj<'lean>, &str, &str, &str, u64, usize, usize, usize),
1565                LeanIo<Obj<'lean>>,
1566            > = unsafe { LeanExported::from_function_address(self.runtime(), address) };
1567            let t = Instant::now();
1568            let result = call.call(
1569                self.environment.clone(),
1570                source,
1571                options.namespace_context_str(),
1572                options.file_label_str(),
1573                options.heartbeats(),
1574                options.diagnostic_byte_limit_usize(),
1575                handle,
1576                trampoline,
1577            );
1578            self.record_call(0, t.elapsed());
1579            bridge.decode(result?)
1580        } else {
1581            let address = self.capabilities.symbols().kernel_check;
1582            // SAFETY: per the SessionSymbols::resolve invariant; signature
1583            // is `(Environment, String, String, String, UInt64, USize) ->
1584            // IO KernelOutcome`.
1585            let call: LeanExported<
1586                'lean,
1587                '_,
1588                (Obj<'lean>, &str, &str, &str, u64, usize),
1589                LeanIo<LeanKernelOutcome<'lean>>,
1590            > = unsafe { LeanExported::from_function_address(self.runtime(), address) };
1591            let t = Instant::now();
1592            let result = call.call(
1593                self.environment.clone(),
1594                source,
1595                options.namespace_context_str(),
1596                options.file_label_str(),
1597                options.heartbeats(),
1598                options.diagnostic_byte_limit_usize(),
1599            );
1600            self.record_call(0, t.elapsed());
1601            result
1602        }
1603    }
1604
1605    /// Re-validate a previously captured [`LeanEvidence`] against the
1606    /// session's imported environment, returning the kernel's current
1607    /// verdict.
1608    ///
1609    /// The handle was produced by an earlier
1610    /// [`Self::kernel_check`] call against this same environment and
1611    /// carries the kernel-accepted `Lean.Declaration` opaquely. The
1612    /// session never installs that declaration into its stored
1613    /// environment, so re-checking against the unchanged environment
1614    /// is the supported way to ask "is this evidence still valid?" —
1615    /// the kernel runs fresh.
1616    ///
1617    /// The returned [`EvidenceStatus`] mirrors
1618    /// [`LeanKernelOutcome::status`]: `Checked` on success, `Rejected`
1619    /// if the kernel now refuses the declaration, `Unavailable` if
1620    /// the Lean shim caught an `IO` exception. The Lean fixture does
1621    /// not currently emit `Unsupported` from this path — `Unsupported`
1622    /// only fires during the initial classification in
1623    /// `kernel_check`.
1624    ///
1625    /// # Errors
1626    ///
1627    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean shim raises
1628    /// through `IO` outside of its own `try` (an unexpected internal
1629    /// failure that the shim did not classify). Returns
1630    /// [`lean_rs::LeanError::Host`] with stage [`HostStage::Conversion`] if the
1631    /// return value does not decode as a four-tag
1632    /// [`EvidenceStatus`] inductive.
1633    pub fn check_evidence(
1634        &mut self,
1635        handle: &LeanEvidence<'lean>,
1636        cancellation: Option<&LeanCancellationToken>,
1637    ) -> LeanResult<EvidenceStatus> {
1638        let _span = tracing::debug_span!(
1639            target: "lean_rs",
1640            "lean_rs.host.session.check_evidence",
1641        )
1642        .entered();
1643        check_cancellation(cancellation)?;
1644        let address = self.capabilities.symbols().check_evidence;
1645        // SAFETY: per the SessionSymbols::resolve invariant; signature
1646        // is `(Environment, Evidence) -> IO EvidenceStatus`.
1647        let call: LeanExported<'lean, '_, (Obj<'lean>, LeanEvidence<'lean>), LeanIo<EvidenceStatus>> =
1648            unsafe { LeanExported::from_function_address(self.runtime(), address) };
1649        let t = Instant::now();
1650        let result = call.call(self.environment.clone(), handle.clone());
1651        self.record_call(0, t.elapsed());
1652        result
1653    }
1654
1655    /// Project a previously captured [`LeanEvidence`] into a bounded
1656    /// [`ProofSummary`] for diagnostics or storage.
1657    ///
1658    /// The Lean shim renders the captured declaration's name, kind,
1659    /// and type expression as three byte-bounded `String`s — no
1660    /// `Lean.Expr` or proof term crosses the FFI boundary. The
1661    /// summary is computed on demand (not at
1662    /// [`Self::kernel_check`] time) because most callers only ever
1663    /// inspect the [`EvidenceStatus`] tag and would pay the
1664    /// pretty-print cost for nothing.
1665    ///
1666    /// Strings on the returned summary are display text. They are not
1667    /// semantic keys; route equality comparisons through a
1668    /// Lean-authored equality export.
1669    ///
1670    /// # Errors
1671    ///
1672    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean shim raises
1673    /// through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
1674    /// [`HostStage::Conversion`] if the return value does not decode
1675    /// as a three-field [`ProofSummary`] structure.
1676    pub fn summarize_evidence(
1677        &mut self,
1678        handle: &LeanEvidence<'lean>,
1679        cancellation: Option<&LeanCancellationToken>,
1680    ) -> LeanResult<ProofSummary> {
1681        let _span = tracing::debug_span!(
1682            target: "lean_rs",
1683            "lean_rs.host.session.summarize_evidence",
1684        )
1685        .entered();
1686        check_cancellation(cancellation)?;
1687        let address = self.capabilities.symbols().evidence_summary;
1688        // SAFETY: per the SessionSymbols::resolve invariant; signature
1689        // is `(Environment, Evidence) -> IO ProofSummary`.
1690        let call: LeanExported<'lean, '_, (Obj<'lean>, LeanEvidence<'lean>), LeanIo<ProofSummary>> =
1691            unsafe { LeanExported::from_function_address(self.runtime(), address) };
1692        let t = Instant::now();
1693        let result = call.call(self.environment.clone(), handle.clone());
1694        self.record_call(0, t.elapsed());
1695        result
1696    }
1697
1698    /// Invoke a registered bounded [`MetaM`](https://leanprover.github.io/theorem_proving_in_lean4/)
1699    /// service against the imported environment.
1700    ///
1701    /// The session looks up the service's cached address; if the
1702    /// loaded capability does not export the symbol, the call short-
1703    /// circuits to [`LeanMetaResponse::Unsupported`] with a synthetic
1704    /// host-side diagnostic naming the missing symbol. Otherwise the
1705    /// session constructs a per-call typed [`LeanExported`] handle
1706    /// over the meta service's `(Environment, Req, UInt64, USize,
1707    /// UInt8) -> IO (MetaResponse Resp)` signature and dispatches.
1708    ///
1709    /// The outer [`LeanResult`] surfaces host-stack failures (a Lean
1710    /// `IO`-level exception from the shim itself, or an undecodable
1711    /// return value). The four-way classification — `Ok` / `Failed` /
1712    /// `TimeoutOrHeartbeat` / `Unsupported` — lives in the inner
1713    /// [`LeanMetaResponse`].
1714    ///
1715    /// # Errors
1716    ///
1717    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean shim raises
1718    /// through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
1719    /// [`HostStage::Conversion`] if the return value does not decode
1720    /// into [`LeanMetaResponse<Resp>`].
1721    pub fn run_meta<Req, Resp>(
1722        &mut self,
1723        service: &LeanMetaService<Req, Resp>,
1724        request: Req,
1725        options: &LeanMetaOptions,
1726        cancellation: Option<&LeanCancellationToken>,
1727    ) -> LeanResult<LeanMetaResponse<Resp>>
1728    where
1729        Req: lean_rs::abi::traits::LeanAbi<'lean>,
1730        Resp: TryFromLean<'lean>,
1731    {
1732        let _span = tracing::debug_span!(
1733            target: "lean_rs",
1734            "lean_rs.host.session.run_meta",
1735            service = service.name(),
1736            heartbeats = options.heartbeats(),
1737            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1738        )
1739        .entered();
1740        check_cancellation(cancellation)?;
1741        let Some(address) = self.capabilities.symbols().meta_address_by_name(service.name()) else {
1742            let message = format!(
1743                "meta service '{}' is not exported by the loaded capability",
1744                service.name()
1745            );
1746            return Ok(LeanMetaResponse::Unsupported(LeanElabFailure::synthetic(
1747                message,
1748                "<host>".to_owned(),
1749            )));
1750        };
1751        // SAFETY: per the SessionSymbols::resolve invariant — the
1752        // address (when present) resolves a Lake-emitted function
1753        // whose signature is pinned in the capability contract table
1754        // above: `(Environment, Req, UInt64, USize, UInt8) -> IO
1755        // (MetaResponse Resp)`. `Req: LeanAbi<'lean>` and `Resp:
1756        // TryFromLean<'lean>` line up with the per-arg `CRepr` and the
1757        // `LeanIo` decoder.
1758        let call: LeanExported<'lean, '_, (Obj<'lean>, Req, u64, usize, u8), LeanIo<LeanMetaResponse<Resp>>> =
1759            unsafe { LeanExported::from_function_address(self.runtime(), address) };
1760        let t = Instant::now();
1761        let result = call.call(
1762            self.environment.clone(),
1763            request,
1764            options.heartbeats(),
1765            options.diagnostic_byte_limit_usize(),
1766            options.transparency_byte(),
1767        );
1768        self.record_call(0, t.elapsed());
1769        result
1770    }
1771
1772    /// Look up many declarations in one Lean traversal.
1773    ///
1774    /// Equivalent to calling [`Self::query_declaration`] in a loop over
1775    /// `names`, except that the entire batch crosses the FFI boundary
1776    /// exactly once: one `Array Name` allocation in, one
1777    /// `Array (Option Declaration)` allocation out. The Lean shim folds
1778    /// the singular `envQueryDeclaration` across the input array, so the
1779    /// iteration semantics are identical to a Rust-side fold over the
1780    /// singular path — a missing name still errors the batch.
1781    ///
1782    /// Names are still resolved through the capability's
1783    /// `name_from_string` shim, one [`lean_rs::LeanName`] handle per
1784    /// input. The metric impact is `names.len() + 1` recorded FFI calls
1785    /// for a batch of `names.len()` items, versus `2 * names.len()` for
1786    /// the same workload through [`Self::query_declaration`].
1787    ///
1788    /// # Errors
1789    ///
1790    /// Returns [`lean_rs::LeanError::Host`] with stage [`HostStage::Conversion`]
1791    /// on the first name that is not present in the imported
1792    /// environment, with the missing name in the diagnostic. Returns
1793    /// [`lean_rs::LeanError::LeanException`] if the Lean-side bulk shim raises
1794    /// through `IO`.
1795    pub fn query_declarations_bulk(
1796        &mut self,
1797        names: &[&str],
1798        cancellation: Option<&LeanCancellationToken>,
1799        progress: Option<&dyn LeanProgressSink>,
1800    ) -> LeanResult<Vec<LeanDeclaration<'lean>>> {
1801        let _span = tracing::debug_span!(
1802            target: "lean_rs",
1803            "lean_rs.host.session.query_declarations_bulk",
1804            batch_size = names.len(),
1805        )
1806        .entered();
1807        if names.is_empty() {
1808            return Ok(Vec::new());
1809        }
1810        check_cancellation(cancellation)?;
1811        if cancellation.is_some() {
1812            let started = Instant::now();
1813            let mut out = Vec::with_capacity(names.len());
1814            let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1815            for (idx, name) in names.iter().enumerate() {
1816                check_cancellation(cancellation)?;
1817                out.push(self.query_declaration(name, cancellation)?);
1818                report_progress(
1819                    progress,
1820                    "query_declarations_bulk",
1821                    u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
1822                    total,
1823                    started,
1824                )?;
1825            }
1826            return Ok(out);
1827        }
1828        let prepare_started = Instant::now();
1829        let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1830        let mut name_handles: Vec<LeanName<'lean>> = Vec::with_capacity(names.len());
1831        for (idx, name) in names.iter().enumerate() {
1832            name_handles.push(self.make_name(name, cancellation)?);
1833            report_progress(
1834                progress,
1835                "prepare_names",
1836                u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
1837                total,
1838                prepare_started,
1839            )?;
1840        }
1841        check_cancellation(cancellation)?;
1842        let raw = if let Some(sink) = progress {
1843            let bridge = ProgressBridge::new(sink, "query_declarations_bulk", total)?;
1844            let (handle, trampoline) = bridge.abi_parts();
1845            let address = self.capabilities.symbols().env_query_declarations_bulk_progress;
1846            // SAFETY: per the SessionSymbols::resolve invariant; signature
1847            // is `(Environment, Array Name, USize, USize) ->
1848            // IO (Except UInt8 (Array (Option Declaration)))`.
1849            let call: LeanExported<'lean, '_, (Obj<'lean>, Vec<LeanName<'lean>>, usize, usize), LeanIo<Obj<'lean>>> =
1850                unsafe { LeanExported::from_function_address(self.runtime(), address) };
1851            let t = Instant::now();
1852            let result = call.call(self.environment.clone(), name_handles, handle, trampoline);
1853            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1854            self.record_call(batch_len, t.elapsed());
1855            bridge.decode::<Vec<Option<LeanDeclaration<'lean>>>>(result?)?
1856        } else {
1857            let address = self.capabilities.symbols().env_query_declarations_bulk;
1858            // SAFETY: per the SessionSymbols::resolve invariant; signature
1859            // is `(Environment, Array Name) -> IO (Array (Option Declaration))`.
1860            let call: LeanExported<
1861                'lean,
1862                '_,
1863                (Obj<'lean>, Vec<LeanName<'lean>>),
1864                LeanIo<Vec<Option<LeanDeclaration<'lean>>>>,
1865            > = unsafe { LeanExported::from_function_address(self.runtime(), address) };
1866            let t = Instant::now();
1867            let result = call.call(self.environment.clone(), name_handles);
1868            let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1869            self.record_call(batch_len, t.elapsed());
1870            result?
1871        };
1872        let mut out: Vec<LeanDeclaration<'lean>> = Vec::with_capacity(raw.len());
1873        for (slot, name) in raw.into_iter().zip(names.iter()) {
1874            match slot {
1875                Some(decl) => out.push(decl),
1876                None => {
1877                    return Err(lean_rs::abi::traits::conversion_error(format!(
1878                        "declaration '{name}' not found in imported environment"
1879                    )));
1880                }
1881            }
1882        }
1883        Ok(out)
1884    }
1885
1886    /// Parse and elaborate many independent Lean terms in one Lean
1887    /// traversal.
1888    ///
1889    /// Per-source `Result<LeanExpr, LeanElabFailure>` shape matches
1890    /// [`Self::elaborate`] exactly: outer [`LeanResult`] surfaces
1891    /// host-stack failures, inner per-source `Result` distinguishes
1892    /// successful elaboration from elaborator-reported diagnostics. A
1893    /// caller treating the bulk path as a fold over the singular path
1894    /// sees no semantic surprise.
1895    ///
1896    /// The `expected_type` parameter is **not** carried by the bulk
1897    /// shape: per-source expectations would force a parallel
1898    /// `&[Option<&LeanExpr>]` array, and no in-tree caller has earned
1899    /// the surface. Use [`Self::elaborate`] for individual terms with
1900    /// expected types.
1901    ///
1902    /// The heartbeat and diagnostic-byte budgets in `options` apply
1903    /// once each per source (the Lean shim builds fresh
1904    /// [`Lean.Options`](https://leanprover.github.io/) per item via the
1905    /// same `hostElaborate` path), so the per-batch upper bound on
1906    /// elapsed CPU work is `sources.len() * options.heartbeats()`.
1907    ///
1908    /// # Errors
1909    ///
1910    /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side bulk shim
1911    /// raises through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
1912    /// [`HostStage::Conversion`] if the Lean return value does not
1913    /// decode into a `Vec<Result<LeanExpr, LeanElabFailure>>`.
1914    pub fn elaborate_bulk(
1915        &mut self,
1916        sources: &[&str],
1917        options: &LeanElabOptions,
1918        cancellation: Option<&LeanCancellationToken>,
1919        progress: Option<&dyn LeanProgressSink>,
1920    ) -> LeanResult<Vec<Result<LeanExpr<'lean>, LeanElabFailure>>> {
1921        let _span = tracing::debug_span!(
1922            target: "lean_rs",
1923            "lean_rs.host.session.elaborate_bulk",
1924            batch_size = sources.len(),
1925            heartbeats = options.heartbeats(),
1926            diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1927        )
1928        .entered();
1929        if sources.is_empty() {
1930            return Ok(Vec::new());
1931        }
1932        check_cancellation(cancellation)?;
1933        if cancellation.is_some() {
1934            let started = Instant::now();
1935            let total = Some(u64::try_from(sources.len()).unwrap_or(u64::MAX));
1936            let mut out = Vec::with_capacity(sources.len());
1937            for (idx, source) in sources.iter().enumerate() {
1938                check_cancellation(cancellation)?;
1939                out.push(self.elaborate(source, None, options, cancellation)?);
1940                report_progress(
1941                    progress,
1942                    "elaborate_bulk",
1943                    u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
1944                    total,
1945                    started,
1946                )?;
1947            }
1948            return Ok(out);
1949        }
1950        let sources_owned: Vec<String> = sources.iter().map(|&s| s.to_owned()).collect();
1951        if let Some(sink) = progress {
1952            let total = Some(u64::try_from(sources.len()).unwrap_or(u64::MAX));
1953            let bridge = ProgressBridge::new(sink, "elaborate_bulk", total)?;
1954            let (handle, trampoline) = bridge.abi_parts();
1955            let address = self.capabilities.symbols().elaborate_bulk_progress;
1956            // SAFETY: per the SessionSymbols::resolve invariant; signature
1957            // is `(Environment, Array String, String, String, UInt64,
1958            // USize, USize, USize) -> IO (Except UInt8 (Array (Except
1959            // ElabFailure Expr)))`.
1960            let call: LeanExported<
1961                'lean,
1962                '_,
1963                (Obj<'lean>, Vec<String>, &str, &str, u64, usize, usize, usize),
1964                LeanIo<Obj<'lean>>,
1965            > = unsafe { LeanExported::from_function_address(self.runtime(), address) };
1966            let t = Instant::now();
1967            let result = call.call(
1968                self.environment.clone(),
1969                sources_owned,
1970                options.namespace_context_str(),
1971                options.file_label_str(),
1972                options.heartbeats(),
1973                options.diagnostic_byte_limit_usize(),
1974                handle,
1975                trampoline,
1976            );
1977            let batch_len = u64::try_from(sources.len()).unwrap_or(u64::MAX);
1978            self.record_call(batch_len, t.elapsed());
1979            bridge.decode(result?)
1980        } else {
1981            let address = self.capabilities.symbols().elaborate_bulk;
1982            // SAFETY: per the SessionSymbols::resolve invariant; signature
1983            // is `(Environment, Array String, String, String, UInt64, USize)
1984            // -> IO (Array (Except ElabFailure Expr))`.
1985            let call: LeanExported<
1986                'lean,
1987                '_,
1988                (Obj<'lean>, Vec<String>, &str, &str, u64, usize),
1989                LeanIo<Vec<Result<LeanExpr<'lean>, LeanElabFailure>>>,
1990            > = unsafe { LeanExported::from_function_address(self.runtime(), address) };
1991            let t = Instant::now();
1992            let result = call.call(
1993                self.environment.clone(),
1994                sources_owned,
1995                options.namespace_context_str(),
1996                options.file_label_str(),
1997                options.heartbeats(),
1998                options.diagnostic_byte_limit_usize(),
1999            );
2000            let batch_len = u64::try_from(sources.len()).unwrap_or(u64::MAX);
2001            self.record_call(batch_len, t.elapsed());
2002            result
2003        }
2004    }
2005
2006    /// Look up and invoke a capability-exported function by name with a
2007    /// typed argument tuple and a typed result decoder.
2008    ///
2009    /// This is the transport-neutral escape hatch for capability dylibs
2010    /// that export Lean functions beyond the twenty-eight session-fixed
2011    /// symbols. The conversion bounds — [`LeanArgs`] on the argument
2012    /// tuple and [`DecodeCallResult`] on the result — are the same
2013    /// bounds [`lean_rs::module::LeanModule::exported`] uses, so an
2014    /// IO-returning Lean capability is invoked with `R = LeanIo<T>`
2015    /// (fused `decode_io` + `T::try_from_lean`) and a pure capability
2016    /// with `R = T` for `T: LeanAbi`. The sealed traits stay invisible
2017    /// at the call site; the bound is satisfied automatically.
2018    ///
2019    /// Function-only: nullary-constant globals are not capabilities.
2020    /// Reach a Lean nullary-constant global directly through
2021    /// [`lean_rs::module::LeanModule::exported`] if you need one. The
2022    /// symbol address is resolved on every call (one `dlsym` per
2023    /// invocation); for hot capabilities, prefer pre-resolving via
2024    /// `LeanModule::exported` and caching the [`lean_rs::module::LeanExported`]
2025    /// handle.
2026    ///
2027    /// # Errors
2028    ///
2029    /// Returns [`lean_rs::LeanError::Host`] with stage [`HostStage::Link`] if
2030    /// `name` does not resolve as a function symbol in the capability
2031    /// dylib. Returns [`lean_rs::LeanError::LeanException`] when the underlying
2032    /// Lean export raises through `IO` (only possible when
2033    /// `R = LeanIo<_>`). Returns [`lean_rs::LeanError::Host`] with stage
2034    /// [`HostStage::Conversion`] when the return value does not decode
2035    /// into the declared `R::Output`.
2036    pub fn call_capability<Args, R>(
2037        &mut self,
2038        name: &str,
2039        args: Args,
2040        cancellation: Option<&LeanCancellationToken>,
2041    ) -> LeanResult<R::Output>
2042    where
2043        Args: LeanArgs<'lean>,
2044        R: DecodeCallResult<'lean>,
2045    {
2046        let _span = tracing::debug_span!(
2047            target: "lean_rs",
2048            "lean_rs.host.session.call_capability",
2049            symbol = name,
2050            arity = Args::ARITY,
2051        )
2052        .entered();
2053        check_cancellation(cancellation)?;
2054        let address = self.capabilities.library().resolve_function_symbol(name)?;
2055        check_cancellation(cancellation)?;
2056        // SAFETY: `resolve_function_symbol` resolved an address inside
2057        // the capability's `LeanLibrary<'lean>` (the dylib outlives the
2058        // session via the `'c` borrow). `Args: LeanArgs<'lean>` and
2059        // `R: DecodeCallResult<'lean>` line up with Lake's emitted C
2060        // ABI for the named symbol. The caller is responsible for
2061        // matching the Lean export's actual signature.
2062        let call: LeanExported<'lean, '_, Args, R> =
2063            unsafe { LeanExported::from_function_address(self.runtime(), address) };
2064        let t = Instant::now();
2065        let result = Args::invoke(&call, args);
2066        self.record_call(0, t.elapsed());
2067        result
2068    }
2069
2070    fn runtime(&self) -> &'lean lean_rs::LeanRuntime {
2071        self.capabilities.host().runtime()
2072    }
2073
2074    /// Build a `LeanName` from a dotted Rust string via the capability's
2075    /// `Name.toName` shim.
2076    fn make_name(&self, name: &str, cancellation: Option<&LeanCancellationToken>) -> LeanResult<LeanName<'lean>> {
2077        check_cancellation(cancellation)?;
2078        let address = self.capabilities.symbols().name_from_string;
2079        // SAFETY: per the SessionSymbols::resolve invariant; signature
2080        // is `String -> Name` (pure, not IO).
2081        let to_name: LeanExported<'lean, '_, (&str,), LeanName<'lean>> =
2082            unsafe { LeanExported::from_function_address(self.runtime(), address) };
2083        let t = Instant::now();
2084        let result = to_name.call(name);
2085        self.record_call(0, t.elapsed());
2086        result
2087    }
2088}