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