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