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