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