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//! manifest-checked typed host-shim binding cached on the session—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 thirty-two **mandatory** `@[export]` symbols and may export ten
15//! **optional** symbols (checked when session bindings are constructed)—
16//! five bounded `MetaM` services plus declaration inspection, module-query
17//! entry points, and cache control:
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_session_import_profile` | yes | `Array String -> Array String -> Bool -> UInt8 -> Bool -> Bool -> Bool -> String -> IO Environment` |
24//! | `lean_rs_host_session_import_profile_progress` | yes | `Array String -> Array String -> Bool -> UInt8 -> USize -> USize -> IO (Except UInt8 Environment)` |
25//! | `lean_rs_host_env_import_stats` | yes | `Environment -> String -> Bool -> IO ImportStats` |
26//! | `lean_rs_host_bracketed_import_query` | yes | `Array String -> Array String -> Array String -> USize -> USize -> IO (Except UInt8 String)` |
27//! | `lean_rs_host_name_from_string` | yes | `String -> Name` |
28//! | `lean_rs_host_name_to_string` | yes | `Name -> String` |
29//! | `lean_rs_host_env_query_declaration` | yes | `Environment -> Name -> IO (Option Declaration)` |
30//! | `lean_rs_host_env_query_declarations_bulk` | yes | `Environment -> Array Name -> IO (Array (Option Declaration))` |
31//! | `lean_rs_host_env_query_declarations_bulk_progress` | yes | `Environment -> Array Name -> USize -> USize -> IO (Except UInt8 (Array (Option Declaration)))` |
32//! | `lean_rs_host_env_list_declarations` | yes | `Environment -> IO (Array Name)` |
33//! | `lean_rs_host_env_list_declarations_filtered` | yes | `Environment -> DeclarationFilter -> IO (Array Name)` |
34//! | `lean_rs_host_env_list_declarations_filtered_progress` | yes | `Environment -> DeclarationFilter -> USize -> USize -> IO (Except UInt8 (Array Name))` |
35//! | `lean_rs_host_env_declaration_source_range` | yes | `Environment -> Name -> Array String -> IO (Option SourceRange)` |
36//! | `lean_rs_host_env_declaration_type` | yes | `Environment -> Name -> IO (Option Expr)` |
37//! | `lean_rs_host_env_declaration_type_bulk` | yes | `Environment -> Array String -> IO (Array (Option Expr))` |
38//! | `lean_rs_host_env_declaration_type_bulk_progress` | yes | `Environment -> Array String -> USize -> USize -> IO (Except UInt8 (Array (Option Expr)))` |
39//! | `lean_rs_host_env_declaration_kind` | yes | `Environment -> Name -> IO String` |
40//! | `lean_rs_host_env_declaration_kind_bulk` | yes | `Environment -> Array String -> IO (Array String)` |
41//! | `lean_rs_host_env_declaration_kind_bulk_progress` | yes | `Environment -> Array String -> USize -> USize -> IO (Except UInt8 (Array String))` |
42//! | `lean_rs_host_env_declaration_name` | yes | `Environment -> Name -> IO String` |
43//! | `lean_rs_host_env_declaration_name_bulk` | yes | `Environment -> Array String -> IO (Array String)` |
44//! | `lean_rs_host_env_declaration_name_bulk_progress` | yes | `Environment -> Array String -> USize -> USize -> IO (Except UInt8 (Array String))` |
45//! | `lean_rs_host_env_search_declarations` | yes | `Environment -> DeclarationSearchRequest -> Array String -> IO DeclarationSearchResult` |
46//! | `lean_rs_host_env_inspect_declaration` | optional | `Environment -> DeclarationInspectionRequest -> Array String -> IO DeclarationInspectionResult` |
47//! | `lean_rs_host_env_expr_to_string_raw` | yes | `Expr -> String` |
48//! | `lean_rs_host_elaborate` | yes | `Environment -> String -> Option Expr -> String -> String -> UInt64 -> USize -> IO (Except ElabFailure Expr)` |
49//! | `lean_rs_host_elaborate_bulk` | yes | `Environment -> Array String -> String -> String -> UInt64 -> USize -> IO (Array (Except ElabFailure Expr))` |
50//! | `lean_rs_host_elaborate_bulk_progress` | yes | `Environment -> Array String -> String -> String -> UInt64 -> USize -> USize -> USize -> IO (Except UInt8 (Array (Except ElabFailure Expr)))` |
51//! | `lean_rs_host_kernel_check` | yes | `Environment -> String -> String -> String -> UInt64 -> USize -> IO KernelOutcome` |
52//! | `lean_rs_host_kernel_check_progress` | yes | `Environment -> String -> String -> String -> UInt64 -> USize -> USize -> USize -> IO (Except UInt8 KernelOutcome)` |
53//! | `lean_rs_host_check_evidence` | yes | `Environment -> Evidence -> IO EvidenceStatus` |
54//! | `lean_rs_host_evidence_summary` | yes | `Environment -> Evidence -> IO ProofSummary` |
55//! | `lean_rs_host_meta_infer_type` | optional | `Environment -> Expr -> UInt64 -> USize -> UInt8 -> IO (MetaResponse Expr)` |
56//! | `lean_rs_host_meta_whnf` | optional | `Environment -> Expr -> UInt64 -> USize -> UInt8 -> IO (MetaResponse Expr)` |
57//! | `lean_rs_host_meta_heartbeat_burn` | optional | `Environment -> Expr -> UInt64 -> USize -> UInt8 -> IO (MetaResponse Expr)` |
58//! | `lean_rs_host_meta_is_def_eq` | optional | `Environment -> (Expr × Expr × UInt8) -> UInt64 -> USize -> UInt8 -> IO (MetaResponse Bool)` |
59//! | `lean_rs_host_meta_pp_expr` | optional | `Environment -> Expr -> UInt64 -> USize -> UInt8 -> IO (MetaResponse String)` |
60//! | `lean_rs_host_process_module_query` | optional | `Environment -> String -> ModuleQuery -> String -> String -> UInt64 -> USize -> IO ModuleQueryOutcome` |
61//! | `lean_rs_host_process_module_query_batch` | optional | `Environment -> String -> Array ModuleQuerySelector -> ModuleQueryOutputBudgets -> String -> String -> UInt64 -> USize -> IO ModuleQueryBatchOutcome` |
62//! | `lean_rs_host_process_module_query_batch_cached` | optional | `Environment -> String -> Array ModuleQuerySelector -> ModuleQueryOutputBudgets -> String -> String -> UInt64 -> USize -> String -> IO ModuleQueryBatchCachedOutcome`|
63//! | `lean_rs_host_verify_declaration_batch` | optional | `Environment -> DeclarationVerificationBatchRequest -> String -> String -> UInt64 -> USize -> IO DeclarationVerificationBatchOutcome` |
64//! | `lean_rs_host_clear_module_snapshot_cache` | optional | `Unit -> IO ModuleSnapshotCacheClearResult` |
65//!
66//! Missing **mandatory** symbols surface at `load_capabilities` as
67//! [`lean_rs::HostStage::Link`]—failures bind to the capability's load,
68//! not to the first query. Missing **optional** symbols degrade
69//! gracefully: [`LeanSession::run_meta`] returns
70//! [`crate::host::meta::LeanMetaResponse::Unsupported`] against a service whose
71//! binding did not resolve, [`LeanSession::process_module_query`]
72//! returns [`crate::host::process::ModuleQueryOutcome::Unsupported`],
73//! and the rest of the capability stays usable.
74//! The evidence-side pair (`check_evidence`, `evidence_summary`) is
75//! mandatory because any capability that produces a `LeanEvidence`
76//! handle via `kernel_check` must also be able to re-validate and
77//! summarize it: the missing-symbol case defines no recoverable
78//! caller behaviour, so the error is folded into capability load
79//! rather than into every call site.
80//!
81//! Capability contracts are extended additively over time: any future
82//! capability symbol becomes a new mandatory or optional row in the
83//! table above without renaming or removing existing ones.
84//!
85//! ## Per-session metrics
86//!
87//! Every [`LeanSession`] carries a [`SessionStats`] counter that
88//! accumulates dispatch events (one FFI call per typed query, plus
89//! per-item counts for the bulk methods) and the wall time spent inside
90//! `.call(...)`. Snapshot via [`LeanSession::stats`]; reset by dropping
91//! the session. `import` itself is **not** counted as a query FFI call
92//!—pool reuse vs. fresh import is tracked at the
93//! [`crate::host::pool::SessionPool`] level instead.
94//!
95//! ## Cancellation
96//!
97//! Every public method that can enter Lean accepts
98//! `Option<&LeanCancellationToken>`. `None` keeps the fastest path and,
99//! for bulk methods, keeps the single Lean-side bulk dispatch. `Some`
100//! checks the token before host-controlled FFI dispatches; cancellable
101//! bulk methods switch to per-item dispatch so they can also check
102//! between items. Cancellation is cooperative and cannot interrupt a
103//! Lean call already in progress.
104//!
105//! ## Progress
106//!
107//! Long-running session operations also accept
108//! `Option<&dyn LeanProgressSink>`. `None` allocates no callback handle
109//! and preserves the existing fast path. `Some(sink)` delivers
110//! phase-local [`crate::host::progress::LeanProgressEvent`] values on
111//! the Lean-bound worker thread. A progress sink must not call back into
112//! the same session.
113//!
114//! The Rust side passes the `.olean` search path (resolved by
115//! [`crate::host::lake::LakeProject`]) as the first argument to the
116//! profile-aware import shim; the Lean shim only has to call
117//! `Lean.initSearchPath` and `Lean.importModules` on it. Path-layout
118//! knowledge stays in Rust.
119//!
120//! ## Lifetime story
121//!
122//! - `LeanSession<'lean, 'c>` borrows `&'c LeanCapabilities<'lean, '_>`.
123//! - The session's owned `Obj<'lean>` is independent of `'c`; it carries
124//! one Lean refcount on the imported environment, anchored to the
125//! runtime.
126//! - `HostShimBindings<'lean, 'c>` borrows from the manifest-backed shim
127//! capability owned by `LeanCapabilities`; its typed call handles live
128//! exactly as long as the session borrow.
129
130// `run_meta` is `pub` but bounded on `lean_rs::abi::traits::{LeanAbi, TryFromLean}`.
131// `LeanAbi` is sealed-public; `TryFromLean` is `pub(crate)`. The bound is a
132// crate-internal compatibility requirement, not a downstream extension point
133// (the meta-service registry is closed by `host::meta::service`). Same
134// precedent as `module::exported.rs`.
135#![allow(private_bounds, private_interfaces)]
136
137use core::cell::Cell;
138use std::sync::Mutex;
139use std::time::Instant;
140
141use crate::host::cancellation::{LeanCancellationToken, check_cancellation};
142use crate::host::capabilities::LeanCapabilities;
143use crate::host::declaration_search::{
144 DeclarationInspectionRequest, DeclarationInspectionResult, DeclarationSearchRequest, DeclarationSearchResult,
145};
146use crate::host::elaboration::{LeanElabFailure, LeanElabOptions};
147use crate::host::evidence::{EvidenceStatus, LeanEvidence, LeanKernelOutcome, ProofSummary};
148use crate::host::meta::{LeanMetaOptions, LeanMetaResponse, LeanMetaService};
149use crate::host::process::{
150 DeclarationVerificationBatchOutcome, DeclarationVerificationBatchRequest, DeclarationVerificationOutcome,
151 DeclarationVerificationRequest, ModuleQuery, ModuleQueryBatchCachedOutcome, ModuleQueryBatchOutcome,
152 ModuleQueryCachePolicy, ModuleQueryOutcome, ModuleQueryOutputBudgets, ModuleQuerySelector,
153 ModuleSnapshotCacheClearResult, ProofAttemptOutcome, ProofAttemptRequest,
154};
155use crate::host::progress::{LeanProgressSink, ProgressBridge, report_progress};
156use crate::host::shim_bindings::{HostShimBindings, binding_error_to_lean_error};
157use lean_rs::Obj;
158use lean_rs::abi::structure::{alloc_ctor_with_objects, take_ctor_objects, view};
159use lean_rs::abi::traits::{IntoLean, LeanAbi, TryFromLean, conversion_error, sealed};
160#[cfg(doc)]
161use lean_rs::error::HostStage;
162use lean_rs::error::LeanResult;
163use lean_rs::{LeanDeclaration, LeanExpr, LeanName};
164
165// -- SessionStats: per-session dispatch metrics --------------------------
166
167/// Cumulative dispatch metrics for one [`LeanSession`].
168///
169/// Snapshot via [`LeanSession::stats`]. Each typed query method records
170/// one FFI call; the bulk methods also record the per-item batch
171/// size. `elapsed_ns` accumulates the wall time spent inside the inner
172/// `.call(...)` dispatch (measured with [`Instant::now`])—it excludes
173/// Rust-side argument marshaling, name lookup, and result decoding so
174/// the number is comparable across singular and bulk paths.
175///
176/// `import` is **not** counted: import vs. reuse is tracked at the
177/// [`crate::host::pool::SessionPool`] level. Construction of a session
178/// always pays for one import, and that cost is reported by
179/// [`crate::host::pool::PoolStats::imports_performed`] when the session
180/// flows through a pool.
181#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
182pub struct SessionStats {
183 /// Number of typed query FFI calls dispatched through this session,
184 /// counting each singular call once and each bulk call once
185 /// regardless of batch size.
186 pub ffi_calls: u64,
187 /// Cumulative number of per-item entries processed by the bulk
188 /// methods. Singular calls do not contribute. A batch of N items
189 /// adds N here and 1 to [`Self::ffi_calls`].
190 pub batch_items: u64,
191 /// Cumulative nanoseconds spent inside the dispatch `.call(...)`
192 /// across every recorded FFI call.
193 pub elapsed_ns: u64,
194}
195
196/// Lean-native attribution for the imported environment behind a session.
197#[derive(Clone, Debug, Eq, PartialEq)]
198pub struct LeanImportStats {
199 pub direct_import_names: Vec<String>,
200 pub effective_module_count: u64,
201 pub compacted_region_count: u64,
202 pub memory_mapped_region_count: u64,
203 pub compacted_region_bytes: u64,
204 pub memory_mapped_region_bytes: u64,
205 pub non_memory_mapped_region_bytes: u64,
206 pub imported_bytes: u64,
207 pub imported_constant_count: u64,
208 pub extension_count: u64,
209 pub total_imported_extension_entries: u64,
210 pub import_level: String,
211 pub import_all: bool,
212 pub load_exts: bool,
213}
214
215impl<'lean> TryFromLean<'lean> for LeanImportStats {
216 fn try_from_lean(obj: Obj<'lean>) -> LeanResult<Self> {
217 let (
218 effective_module_count,
219 compacted_region_count,
220 memory_mapped_region_count,
221 compacted_region_bytes,
222 memory_mapped_region_bytes,
223 non_memory_mapped_region_bytes,
224 imported_bytes,
225 imported_constant_count,
226 extension_count,
227 total_imported_extension_entries,
228 import_all,
229 load_exts,
230 ) = {
231 let ctor = view(&obj).ctor_shape(0, 2, "ImportStats")?;
232 (
233 ctor.uint64(0, "ImportStats.effectiveModuleCount")?,
234 ctor.uint64(8, "ImportStats.compactedRegionCount")?,
235 ctor.uint64(16, "ImportStats.memoryMappedRegionCount")?,
236 ctor.uint64(24, "ImportStats.compactedRegionBytes")?,
237 ctor.uint64(32, "ImportStats.memoryMappedRegionBytes")?,
238 ctor.uint64(40, "ImportStats.nonMemoryMappedRegionBytes")?,
239 ctor.uint64(48, "ImportStats.importedBytes")?,
240 ctor.uint64(56, "ImportStats.importedConstantCount")?,
241 ctor.uint64(64, "ImportStats.extensionCount")?,
242 ctor.uint64(72, "ImportStats.totalImportedExtensionEntries")?,
243 ctor.bool(80, "ImportStats.importAll")?,
244 ctor.bool(81, "ImportStats.loadExts")?,
245 )
246 };
247 let [direct_import_names, import_level] = take_ctor_objects::<2>(obj, 0, "ImportStats")?;
248 Ok(Self {
249 direct_import_names: Vec::<String>::try_from_lean(direct_import_names)?,
250 effective_module_count,
251 compacted_region_count,
252 memory_mapped_region_count,
253 compacted_region_bytes,
254 memory_mapped_region_bytes,
255 non_memory_mapped_region_bytes,
256 imported_bytes,
257 imported_constant_count,
258 extension_count,
259 total_imported_extension_entries,
260 import_level: String::try_from_lean(import_level)?,
261 import_all,
262 load_exts,
263 })
264 }
265}
266
267impl LeanImportStats {
268 /// Stable compact attribution fragment for memory guardrail diagnostics.
269 #[must_use]
270 pub fn memory_diagnostic(&self) -> String {
271 format!(
272 "import_profile=level:{} import_all:{} load_exts:{} direct_import_count={} direct_imports={} effective_modules={} compacted_regions={} memory_mapped_regions={} compacted_region_bytes={} memory_mapped_region_bytes={} non_memory_mapped_region_bytes={} imported_constants={} extension_entries={}",
273 self.import_level,
274 self.import_all,
275 self.load_exts,
276 self.direct_import_names.len(),
277 self.direct_import_names.join(","),
278 self.effective_module_count,
279 self.compacted_region_count,
280 self.memory_mapped_region_count,
281 self.compacted_region_bytes,
282 self.memory_mapped_region_bytes,
283 self.non_memory_mapped_region_bytes,
284 self.imported_constant_count,
285 self.total_imported_extension_entries
286 )
287 }
288}
289
290/// Closed import levels used by diagnostic profiling imports.
291#[derive(Clone, Copy, Debug, Eq, PartialEq)]
292pub enum LeanImportLevel {
293 Exported,
294 Server,
295 Private,
296}
297
298impl LeanImportLevel {
299 pub const fn as_str(self) -> &'static str {
300 match self {
301 Self::Exported => "exported",
302 Self::Server => "server",
303 Self::Private => "private",
304 }
305 }
306
307 const fn code(self) -> u8 {
308 match self {
309 Self::Exported => 0,
310 Self::Server => 1,
311 Self::Private => 2,
312 }
313 }
314}
315
316/// Closed import profiles for full host sessions.
317///
318/// Profiles intentionally expose names tied to host semantics rather than raw
319/// Lean import knobs. All full-session profiles keep `loadExts := true`; the
320/// no-extension variant remains a profiling-only diagnostic path and never
321/// produces a normal [`LeanSession`]. Because full sessions load environment
322/// extensions, they are not eligible for `Environment.freeRegions` cleanup; use
323/// a worker process boundary to reset Lean import state.
324#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
325pub enum LeanSessionImportProfile {
326 /// Public exported declarations only, with environment extensions loaded.
327 ExportedPublic,
328 /// Server-level import data without `import all`, with environment
329 /// extensions loaded.
330 Server,
331 /// Private-level import data without `import all`, with environment
332 /// extensions loaded.
333 #[default]
334 Private,
335 /// Legacy compatibility import shape: `import all`, private level, with
336 /// environment extensions loaded.
337 FullPrivateCompat,
338}
339
340impl LeanSessionImportProfile {
341 pub const fn label(self) -> &'static str {
342 match self {
343 Self::ExportedPublic => "exported-public",
344 Self::Server => "server",
345 Self::Private => "private",
346 Self::FullPrivateCompat => "full-private-compat",
347 }
348 }
349
350 pub const fn import_all(self) -> bool {
351 matches!(self, Self::FullPrivateCompat)
352 }
353
354 pub const fn import_level(self) -> LeanImportLevel {
355 match self {
356 Self::ExportedPublic => LeanImportLevel::Exported,
357 Self::Server => LeanImportLevel::Server,
358 Self::Private | Self::FullPrivateCompat => LeanImportLevel::Private,
359 }
360 }
361
362 pub const fn load_exts(self) -> bool {
363 true
364 }
365}
366
367/// Closed import-mode matrix for profiling diagnostics.
368#[derive(Clone, Copy, Debug, Eq, PartialEq)]
369pub enum LeanImportProfileMode {
370 FullSession(LeanSessionImportProfile),
371 ExportedNoExts,
372}
373
374impl LeanImportProfileMode {
375 pub const fn label(self) -> &'static str {
376 match self {
377 Self::FullSession(profile) => profile.label(),
378 Self::ExportedNoExts => "exported-no-exts",
379 }
380 }
381
382 pub const fn import_all(self) -> bool {
383 match self {
384 Self::FullSession(profile) => profile.import_all(),
385 Self::ExportedNoExts => false,
386 }
387 }
388
389 pub const fn import_level(self) -> LeanImportLevel {
390 match self {
391 Self::FullSession(profile) => profile.import_level(),
392 Self::ExportedNoExts => LeanImportLevel::Exported,
393 }
394 }
395
396 pub const fn load_exts(self) -> bool {
397 match self {
398 Self::FullSession(profile) => profile.load_exts(),
399 Self::ExportedNoExts => false,
400 }
401 }
402}
403
404/// Lean profiler toggles scoped to diagnostic profiling imports.
405#[derive(Clone, Debug, Default, Eq, PartialEq)]
406pub struct LeanImportProfilerOptions {
407 pub profiler: bool,
408 pub trace_profiler: bool,
409 pub trace_profiler_output: Option<String>,
410}
411
412impl LeanImportProfilerOptions {
413 #[must_use]
414 pub fn new() -> Self {
415 Self::default()
416 }
417
418 #[must_use]
419 pub fn profiler(mut self, enabled: bool) -> Self {
420 self.profiler = enabled;
421 self
422 }
423
424 #[must_use]
425 pub fn trace_profiler(mut self, enabled: bool) -> Self {
426 self.trace_profiler = enabled;
427 self
428 }
429
430 #[must_use]
431 pub fn trace_profiler_output(mut self, path: impl Into<String>) -> Self {
432 self.trace_profiler_output = Some(path.into());
433 self
434 }
435}
436
437// -- Public source-range / filter types ---------------------------------
438
439/// Source range Lean recorded for a declaration.
440///
441/// Coordinates are 1-based at every layer, matching the public
442/// convention of Lean declaration ranges. `file` is the path or module
443/// label Lean/Rust could resolve for the declaration; it is a label for
444/// consumers, not a normalized filesystem guarantee.
445#[derive(Clone, Debug, Eq, PartialEq)]
446pub struct LeanSourceRange {
447 /// File path or module label recorded for the declaration.
448 pub file: String,
449 /// 1-based start line.
450 pub start_line: u32,
451 /// 1-based start column.
452 pub start_column: u32,
453 /// 1-based end line.
454 pub end_line: u32,
455 /// 1-based end column.
456 pub end_column: u32,
457}
458
459impl<'lean> TryFromLean<'lean> for LeanSourceRange {
460 fn try_from_lean(obj: Obj<'lean>) -> LeanResult<Self> {
461 let [file_o, start_line_o, start_column_o, end_line_o, end_column_o] =
462 take_ctor_objects::<5>(obj, 0, "SourceRange")?;
463 Ok(Self {
464 file: String::try_from_lean(file_o)?,
465 start_line: u32::try_from_lean(start_line_o)?,
466 start_column: u32::try_from_lean(start_column_o)?,
467 end_line: u32::try_from_lean(end_line_o)?,
468 end_column: u32::try_from_lean(end_column_o)?,
469 })
470 }
471}
472
473/// Lean-side declaration-listing filter.
474///
475/// The default is tuned for user-facing declaration browsers: include
476/// private names because callers may be indexing the current project,
477/// but drop compiler-generated and internal-detail names that usually
478/// swamp the list with implementation artifacts.
479#[derive(Clone, Copy, Debug, Eq, PartialEq)]
480pub struct LeanDeclarationFilter {
481 /// Keep names Lean marks as private.
482 pub include_private: bool,
483 /// Keep generated names with numeric components.
484 pub include_generated: bool,
485 /// Keep Lean internal-detail names such as `_`, `match_`, `proof_`,
486 /// and similar implementation artifacts.
487 pub include_internal: bool,
488}
489
490impl Default for LeanDeclarationFilter {
491 fn default() -> Self {
492 Self {
493 include_private: true,
494 include_generated: false,
495 include_internal: false,
496 }
497 }
498}
499
500impl<'lean> IntoLean<'lean> for LeanDeclarationFilter {
501 fn into_lean(self, runtime: &'lean lean_rs::LeanRuntime) -> Obj<'lean> {
502 alloc_ctor_with_objects(
503 runtime,
504 0,
505 [
506 self.include_private.into_lean(runtime),
507 self.include_generated.into_lean(runtime),
508 self.include_internal.into_lean(runtime),
509 ],
510 )
511 }
512}
513
514impl<'lean> TryFromLean<'lean> for LeanDeclarationFilter {
515 fn try_from_lean(obj: Obj<'lean>) -> LeanResult<Self> {
516 let [include_private_o, include_generated_o, include_internal_o] =
517 take_ctor_objects::<3>(obj, 0, "DeclarationFilter")?;
518 Ok(Self {
519 include_private: bool::try_from_lean(include_private_o)?,
520 include_generated: bool::try_from_lean(include_generated_o)?,
521 include_internal: bool::try_from_lean(include_internal_o)?,
522 })
523 }
524}
525
526impl sealed::SealedAbi for LeanDeclarationFilter {}
527
528impl<'lean> LeanAbi<'lean> for LeanDeclarationFilter {
529 type CRepr = <Obj<'lean> as LeanAbi<'lean>>::CRepr;
530
531 fn into_c(self, runtime: &'lean lean_rs::LeanRuntime) -> Self::CRepr {
532 self.into_lean(runtime).into_raw()
533 }
534
535 fn from_c(_c: Self::CRepr, _runtime: &'lean lean_rs::LeanRuntime) -> LeanResult<Self> {
536 Err(conversion_error(
537 "LeanDeclarationFilter cannot decode a Lean call result; it is an argument-only type",
538 ))
539 }
540}
541
542// -- LeanSession ---------------------------------------------------------
543
544/// A long-lived Lean session over an imported environment.
545///
546/// Construct via [`LeanCapabilities::session`]. The session owns the
547/// imported `Lean.Environment` privately (never exposed) and dispatches
548/// each typed query through checked host-shim bindings resolved during
549/// construction. Neither [`Send`] nor [`Sync`]: inherited from the
550/// contained `Obj<'lean>` and the borrow of `LeanCapabilities`.
551pub struct LeanSession<'lean, 'c> {
552 capabilities: &'c LeanCapabilities<'lean, 'c>,
553 shims: HostShimBindings<'lean, 'c>,
554 /// The imported `Lean.Environment`. Private—Rust never inspects
555 /// the environment directly; every query routes through a Lean
556 /// capability export.
557 environment: Obj<'lean>,
558 import_stats: LeanImportStats,
559 /// Per-session dispatch metrics. `Cell` because every query method
560 /// takes `&mut self` but the bulk path can also be invoked through a
561 /// shared reference (e.g. inside a fold helper)—keeping the
562 /// counter in `Cell` makes the recording uniform without adding an
563 /// extra `&mut` borrow at each call site.
564 stats: Cell<SessionStats>,
565}
566
567/// Process-wide serialization for [`LeanSession::import`]. See the
568/// comment at the lock-acquire site for the Lean-4.30 race it closes.
569static SESSION_IMPORT_LOCK: Mutex<()> = Mutex::new(());
570
571pub(crate) fn with_session_import_lock<T>(f: impl FnOnce() -> LeanResult<T>) -> LeanResult<T> {
572 let _import_guard = SESSION_IMPORT_LOCK
573 .lock()
574 .unwrap_or_else(|poisoned| poisoned.into_inner());
575 f()
576}
577
578impl<'lean, 'c> LeanSession<'lean, 'c> {
579 pub(crate) fn import_search_paths(capabilities: &LeanCapabilities<'lean, '_>) -> LeanResult<Vec<String>> {
580 let project = capabilities.host().project();
581 let mut search_paths: Vec<String> = project
582 .olean_search_paths()
583 .into_iter()
584 .map(|path| path.to_string_lossy().into_owned())
585 .collect();
586 search_paths.push(
587 crate::host::lake::LakeProject::interop_olean_search_path()?
588 .to_string_lossy()
589 .into_owned(),
590 );
591 search_paths.push(
592 crate::host::lake::LakeProject::shim_olean_search_path()?
593 .to_string_lossy()
594 .into_owned(),
595 );
596 Ok(search_paths)
597 }
598
599 /// Import the named modules into a fresh Lean environment and wrap
600 /// it as a session.
601 ///
602 /// The Lean-side `lean_rs_host_session_import` receives the Lake
603 /// project root (so it can `Lean.initSearchPath` the `.olean`
604 /// directory) and the module-name list, and returns the resulting
605 /// environment. Failures surface as
606 /// [`lean_rs::LeanError::LeanException`] with the message Lean produced.
607 pub(crate) fn import(
608 capabilities: &'c LeanCapabilities<'lean, 'c>,
609 imports: &[&str],
610 cancellation: Option<&LeanCancellationToken>,
611 progress: Option<&dyn LeanProgressSink>,
612 ) -> LeanResult<Self> {
613 Self::import_with_profile(
614 capabilities,
615 imports,
616 LeanSessionImportProfile::default(),
617 cancellation,
618 progress,
619 )
620 }
621
622 pub(crate) fn import_with_profile(
623 capabilities: &'c LeanCapabilities<'lean, 'c>,
624 imports: &[&str],
625 profile: LeanSessionImportProfile,
626 cancellation: Option<&LeanCancellationToken>,
627 progress: Option<&dyn LeanProgressSink>,
628 ) -> LeanResult<Self> {
629 let _span = tracing::info_span!(
630 target: "lean_rs",
631 "lean_rs.host.session.import",
632 profile = profile.label(),
633 imports_len = imports.len(),
634 )
635 .entered();
636 check_cancellation(cancellation)?;
637 let search_paths = Self::import_search_paths(capabilities)?;
638 let imports_owned: Vec<String> = imports.iter().map(|&s| s.to_owned()).collect();
639 // Lean 4.30 strictly enforces `enableInitializersExecution` before
640 // `importModules (loadExts := true)`. The flag is process-global,
641 // but `Lean.withImporting` (wrapped around every import) resets it
642 // on completion—two threads importing concurrently race the
643 // shim's enable→import sequence and the loser sees the flag
644 // cleared by the winner's reset. Serializing the import phase
645 // across the process matches Lean's "single execution thread
646 // accessing the global references" requirement. Sessions operate
647 // concurrently on their own `Environment` values once import
648 // returns; the lock spans only the FFI call.
649 with_session_import_lock(|| {
650 let shims = HostShimBindings::resolve(capabilities.shim_capability())
651 .map_err(|err| binding_error_to_lean_error(&err))?;
652 let environment = if let Some(sink) = progress {
653 let bridge =
654 ProgressBridge::new(sink, "import", Some(u64::try_from(imports.len()).unwrap_or(u64::MAX)))?;
655 let (handle, trampoline) = bridge.abi_parts();
656 let raw = if profile == LeanSessionImportProfile::default() {
657 shims
658 .session_import_progress
659 .call(search_paths, imports_owned, handle, trampoline)?
660 } else {
661 shims.session_import_profile_progress.call(
662 search_paths,
663 imports_owned,
664 profile.import_all(),
665 profile.import_level().code(),
666 handle,
667 trampoline,
668 )?
669 };
670 bridge.decode(raw)?
671 } else if profile == LeanSessionImportProfile::default() {
672 shims.session_import.call(search_paths, imports_owned)?
673 } else {
674 shims.session_import_profile.call(
675 search_paths,
676 imports_owned,
677 profile.import_all(),
678 profile.import_level().code(),
679 profile.load_exts(),
680 false,
681 false,
682 String::new(),
683 )?
684 };
685 let import_stats = shims.env_import_stats.call(
686 environment.clone(),
687 profile.import_level().as_str().to_owned(),
688 profile.load_exts(),
689 )?;
690 Ok(Self {
691 capabilities,
692 shims,
693 environment,
694 import_stats,
695 stats: Cell::new(SessionStats::default()),
696 })
697 })
698 }
699
700 pub(crate) fn import_profiled(
701 capabilities: &'c LeanCapabilities<'lean, 'c>,
702 imports: &[&str],
703 mode: LeanImportProfileMode,
704 profiler_options: &LeanImportProfilerOptions,
705 ) -> LeanResult<Self> {
706 let _span = tracing::info_span!(
707 target: "lean_rs",
708 "lean_rs.host.session.import_profiled",
709 mode = mode.label(),
710 imports_len = imports.len(),
711 )
712 .entered();
713 let search_paths = Self::import_search_paths(capabilities)?;
714 let imports_owned: Vec<String> = imports.iter().map(|&s| s.to_owned()).collect();
715 with_session_import_lock(|| {
716 let shims = HostShimBindings::resolve(capabilities.shim_capability())
717 .map_err(|err| binding_error_to_lean_error(&err))?;
718 let environment = shims.session_import_profile.call(
719 search_paths,
720 imports_owned,
721 mode.import_all(),
722 mode.import_level().code(),
723 mode.load_exts(),
724 profiler_options.profiler,
725 profiler_options.trace_profiler,
726 profiler_options.trace_profiler_output.clone().unwrap_or_default(),
727 )?;
728 let import_stats = shims.env_import_stats.call(
729 environment.clone(),
730 mode.import_level().as_str().to_owned(),
731 mode.load_exts(),
732 )?;
733 Ok(Self {
734 capabilities,
735 shims,
736 environment,
737 import_stats,
738 stats: Cell::new(SessionStats::default()),
739 })
740 })
741 }
742
743 /// Wrap a previously-imported `Lean.Environment` as a fresh
744 /// [`LeanSession`] over `capabilities`.
745 ///
746 /// Crate-private; only [`crate::host::pool::SessionPool::acquire`]
747 /// calls this to recycle a pooled environment under a new
748 /// capability borrow. The returned session's [`SessionStats`] start
749 /// at zero—accumulated counters from the previous owner do not
750 /// leak across pool checkouts.
751 pub(crate) fn from_environment_with_import_stats(
752 capabilities: &'c LeanCapabilities<'lean, 'c>,
753 environment: Obj<'lean>,
754 import_stats: LeanImportStats,
755 ) -> LeanResult<Self> {
756 let shims = HostShimBindings::resolve(capabilities.shim_capability())
757 .map_err(|err| binding_error_to_lean_error(&err))?;
758 Ok(Self {
759 capabilities,
760 shims,
761 environment,
762 import_stats,
763 stats: Cell::new(SessionStats::default()),
764 })
765 }
766
767 /// Consume the session and return its owned `Lean.Environment`.
768 ///
769 /// Crate-private; only [`crate::host::pool::SessionPool`] uses this
770 /// to reclaim the environment when a [`crate::host::pool::PooledSession`]
771 /// drops. The returned `Obj<'lean>` carries one Lean refcount, which
772 /// the pool is responsible for either pushing back into the free
773 /// list (in which case `Drop` runs later when the pool itself
774 /// drops) or releasing immediately (when at capacity).
775 pub(crate) fn into_environment(self) -> Obj<'lean> {
776 self.environment
777 }
778
779 /// Snapshot of this session's accumulated dispatch metrics.
780 ///
781 /// Returns a copy; the counters keep accumulating after the call.
782 /// Use [`SessionStats::default`] to compute a delta across two
783 /// snapshots.
784 #[must_use]
785 pub fn stats(&self) -> SessionStats {
786 self.stats.get()
787 }
788
789 /// Lean-native attribution for this session's imported environment.
790 #[must_use]
791 pub fn import_stats(&self) -> &LeanImportStats {
792 &self.import_stats
793 }
794
795 /// Internal helper: record one FFI call and add `batch` per-item
796 /// entries plus `elapsed` nanoseconds. Singular methods pass
797 /// `batch = 0`; bulk methods pass the input length.
798 fn record_call(&self, batch: u64, elapsed: std::time::Duration) {
799 let mut s = self.stats.get();
800 s.ffi_calls = s.ffi_calls.saturating_add(1);
801 s.batch_items = s.batch_items.saturating_add(batch);
802 s.elapsed_ns = s
803 .elapsed_ns
804 .saturating_add(u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX));
805 self.stats.set(s);
806 }
807
808 fn decode_strings_cached(raw: Vec<Obj<'lean>>) -> LeanResult<Vec<String>> {
809 if raw.is_empty() {
810 return Ok(Vec::new());
811 }
812 let Some(first_key) = raw.first().map(Obj::as_raw_borrowed) else {
813 return Ok(Vec::new());
814 };
815 if raw.iter().all(|obj| obj.as_raw_borrowed() == first_key) {
816 let len = raw.len();
817 let mut raw_iter = raw.into_iter();
818 let Some(first) = raw_iter.next() else {
819 return Ok(Vec::new());
820 };
821 let value = String::try_from_lean(first)?;
822 return Ok(vec![value; len]);
823 }
824 let mut out = Vec::with_capacity(raw.len());
825 for obj in raw {
826 out.push(String::try_from_lean(obj)?);
827 }
828 Ok(out)
829 }
830
831 fn all_equal_name<'a>(names: &'a [&str]) -> Option<&'a str> {
832 let first = *names.first()?;
833 names.iter().all(|name| *name == first).then_some(first)
834 }
835
836 /// Look up a declaration by full Lean name (e.g. `"Nat.zero"`).
837 ///
838 /// # Errors
839 ///
840 /// Returns [`lean_rs::LeanError::Host`] with stage [`HostStage::Conversion`]
841 /// if the name is not present in the imported environment. Returns
842 /// [`lean_rs::LeanError::LeanException`] if the Lean-side query raises.
843 pub fn query_declaration(
844 &mut self,
845 name: &str,
846 cancellation: Option<&LeanCancellationToken>,
847 ) -> LeanResult<LeanDeclaration<'lean>> {
848 let _span = tracing::debug_span!(
849 target: "lean_rs",
850 "lean_rs.host.session.query_declaration",
851 name = name,
852 )
853 .entered();
854 check_cancellation(cancellation)?;
855 let name_handle = self.make_name(name, cancellation)?;
856 check_cancellation(cancellation)?;
857 let t = Instant::now();
858 let result = self
859 .shims
860 .env_query_declaration
861 .call(self.environment.clone(), name_handle);
862 self.record_call(0, t.elapsed());
863 match result? {
864 Some(decl) => Ok(decl),
865 None => Err(lean_rs::abi::traits::conversion_error(format!(
866 "declaration '{name}' not found in imported environment"
867 ))),
868 }
869 }
870
871 /// All declaration names in the imported environment.
872 ///
873 /// Returns a Vec; the environment's `constants` map contains many
874 /// thousands of entries even for a small project (Lean prelude is
875 /// always imported), so prefer [`LeanSession::query_declaration`]
876 /// when you already know the name.
877 ///
878 /// # Errors
879 ///
880 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side query
881 /// raises.
882 pub fn list_declarations(
883 &mut self,
884 cancellation: Option<&LeanCancellationToken>,
885 ) -> LeanResult<Vec<LeanName<'lean>>> {
886 let _span = tracing::debug_span!(
887 target: "lean_rs",
888 "lean_rs.host.session.list_declarations",
889 )
890 .entered();
891 check_cancellation(cancellation)?;
892 let t = Instant::now();
893 let raw = self.shims.env_list_declarations.call(self.environment.clone());
894 self.record_call(0, t.elapsed());
895 raw?.into_iter().map(LeanName::try_from_lean).collect()
896 }
897
898 /// Declaration names matching `filter`.
899 ///
900 /// Filtering runs inside Lean while traversing the environment
901 /// constants table, so Rust only allocates handles for names the
902 /// caller asked to keep.
903 ///
904 /// # Errors
905 ///
906 /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
907 /// already cancelled before dispatch. Returns
908 /// [`lean_rs::LeanError::LeanException`] if the Lean-side query
909 /// raises.
910 pub fn list_declarations_filtered(
911 &mut self,
912 filter: &LeanDeclarationFilter,
913 cancellation: Option<&LeanCancellationToken>,
914 progress: Option<&dyn LeanProgressSink>,
915 ) -> LeanResult<Vec<LeanName<'lean>>> {
916 let _span = tracing::debug_span!(
917 target: "lean_rs",
918 "lean_rs.host.session.list_declarations_filtered",
919 include_private = filter.include_private,
920 include_generated = filter.include_generated,
921 include_internal = filter.include_internal,
922 )
923 .entered();
924 check_cancellation(cancellation)?;
925 let raw = if let Some(sink) = progress {
926 let bridge = ProgressBridge::new(sink, "list_declarations_filtered", None)?;
927 let (handle, trampoline) = bridge.abi_parts();
928 let t = Instant::now();
929 let result = self.shims.env_list_declarations_filtered_progress.call(
930 self.environment.clone(),
931 *filter,
932 handle,
933 trampoline,
934 );
935 self.record_call(0, t.elapsed());
936 bridge.decode::<Vec<Obj<'lean>>>(result?)?
937 } else {
938 let t = Instant::now();
939 let result = self
940 .shims
941 .env_list_declarations_filtered
942 .call(self.environment.clone(), *filter);
943 self.record_call(0, t.elapsed());
944 result?
945 };
946 raw.into_iter().map(LeanName::try_from_lean).collect()
947 }
948
949 /// Source range Lean recorded for `name`.
950 ///
951 /// Returns `Ok(None)` when the name is absent or Lean has no
952 /// declaration range for it. That is normal for synthetic,
953 /// runtime-created, and some compiler-generated declarations.
954 ///
955 /// # Errors
956 ///
957 /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
958 /// already cancelled before dispatch. Returns
959 /// [`lean_rs::LeanError::LeanException`] if the Lean-side query
960 /// raises.
961 pub fn declaration_source_range(
962 &mut self,
963 name: &str,
964 cancellation: Option<&LeanCancellationToken>,
965 ) -> LeanResult<Option<LeanSourceRange>> {
966 let _span = tracing::debug_span!(
967 target: "lean_rs",
968 "lean_rs.host.session.declaration_source_range",
969 name = name,
970 )
971 .entered();
972 check_cancellation(cancellation)?;
973 let name_handle = self.make_name(name, cancellation)?;
974 check_cancellation(cancellation)?;
975 let source_roots = self
976 .capabilities
977 .host()
978 .project()
979 .source_roots()?
980 .into_iter()
981 .map(|path| path.to_string_lossy().into_owned())
982 .collect::<Vec<_>>();
983 check_cancellation(cancellation)?;
984 let t = Instant::now();
985 let result = self
986 .shims
987 .env_declaration_source_range
988 .call(self.environment.clone(), name_handle, source_roots);
989 self.record_call(0, t.elapsed());
990 result
991 }
992
993 /// The declared type of `name`, as an opaque [`LeanExpr`] handle.
994 ///
995 /// Returns `Ok(None)` if the name is not present in the environment.
996 ///
997 /// # Errors
998 ///
999 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side query
1000 /// raises.
1001 pub fn declaration_type(
1002 &mut self,
1003 name: &str,
1004 cancellation: Option<&LeanCancellationToken>,
1005 ) -> LeanResult<Option<LeanExpr<'lean>>> {
1006 let _span = tracing::debug_span!(
1007 target: "lean_rs",
1008 "lean_rs.host.session.declaration_type",
1009 name = name,
1010 )
1011 .entered();
1012 check_cancellation(cancellation)?;
1013 let name_handle = self.make_name(name, cancellation)?;
1014 check_cancellation(cancellation)?;
1015 let t = Instant::now();
1016 let result = self
1017 .shims
1018 .env_declaration_type
1019 .call(self.environment.clone(), name_handle);
1020 self.record_call(0, t.elapsed());
1021 result
1022 }
1023
1024 /// The declared types of `names`, preserving input order.
1025 ///
1026 /// Returns `None` in each slot whose name is not present in the
1027 /// environment. With `cancellation = None`, the whole batch crosses
1028 /// the FFI boundary once and Lean converts the input strings to
1029 /// names internally. With `Some(token)`, this loops through
1030 /// [`Self::declaration_type`] so cancellation can be observed
1031 /// between items; partial results are discarded when cancellation
1032 /// fires.
1033 ///
1034 /// # Errors
1035 ///
1036 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side
1037 /// bulk shim raises through `IO`.
1038 pub fn declaration_type_bulk(
1039 &mut self,
1040 names: &[&str],
1041 cancellation: Option<&LeanCancellationToken>,
1042 progress: Option<&dyn LeanProgressSink>,
1043 ) -> LeanResult<Vec<Option<LeanExpr<'lean>>>> {
1044 let _span = tracing::debug_span!(
1045 target: "lean_rs",
1046 "lean_rs.host.session.declaration_type_bulk",
1047 batch_size = names.len(),
1048 )
1049 .entered();
1050 if names.is_empty() {
1051 return Ok(Vec::new());
1052 }
1053 check_cancellation(cancellation)?;
1054 if cancellation.is_some() {
1055 let started = Instant::now();
1056 let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1057 let mut out = Vec::with_capacity(names.len());
1058 for (idx, name) in names.iter().enumerate() {
1059 check_cancellation(cancellation)?;
1060 out.push(self.declaration_type(name, cancellation)?);
1061 report_progress(
1062 progress,
1063 "declaration_type_bulk",
1064 u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
1065 total,
1066 started,
1067 )?;
1068 }
1069 return Ok(out);
1070 }
1071 if progress.is_none()
1072 && let Some(name) = Self::all_equal_name(names)
1073 {
1074 let names_owned = vec![name.to_owned()];
1075 let t = Instant::now();
1076 let mut result = self
1077 .shims
1078 .env_declaration_type_bulk
1079 .call(self.environment.clone(), names_owned)?;
1080 let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1081 self.record_call(batch_len, t.elapsed());
1082 let value = result.pop().unwrap_or(None);
1083 return Ok(vec![value; names.len()]);
1084 }
1085 let names_owned: Vec<String> = names.iter().map(|&name| name.to_owned()).collect();
1086 if let Some(sink) = progress {
1087 let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1088 let bridge = ProgressBridge::new(sink, "declaration_type_bulk", total)?;
1089 let (handle, trampoline) = bridge.abi_parts();
1090 let t = Instant::now();
1091 let result = self.shims.env_declaration_type_bulk_progress.call(
1092 self.environment.clone(),
1093 names_owned,
1094 handle,
1095 trampoline,
1096 );
1097 let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1098 self.record_call(batch_len, t.elapsed());
1099 bridge.decode(result?)
1100 } else {
1101 let t = Instant::now();
1102 let result = self
1103 .shims
1104 .env_declaration_type_bulk
1105 .call(self.environment.clone(), names_owned);
1106 let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1107 self.record_call(batch_len, t.elapsed());
1108 result
1109 }
1110 }
1111
1112 /// The kind of `name` as a Lean-rendered string
1113 /// (`"axiom"`, `"definition"`, `"theorem"`, `"opaque"`, `"quot"`,
1114 /// `"inductive"`, `"constructor"`, `"recursor"`), or `"missing"`
1115 /// if `name` is not in the environment.
1116 ///
1117 /// # Errors
1118 ///
1119 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side query
1120 /// raises.
1121 pub fn declaration_kind(&mut self, name: &str, cancellation: Option<&LeanCancellationToken>) -> LeanResult<String> {
1122 let _span = tracing::debug_span!(
1123 target: "lean_rs",
1124 "lean_rs.host.session.declaration_kind",
1125 name = name,
1126 )
1127 .entered();
1128 check_cancellation(cancellation)?;
1129 let name_handle = self.make_name(name, cancellation)?;
1130 check_cancellation(cancellation)?;
1131 let t = Instant::now();
1132 let result = self
1133 .shims
1134 .env_declaration_kind
1135 .call(self.environment.clone(), name_handle);
1136 self.record_call(0, t.elapsed());
1137 result
1138 }
1139
1140 /// The declaration kinds of `names`, preserving input order.
1141 ///
1142 /// Each output slot is the same string that [`Self::declaration_kind`]
1143 /// would return for the corresponding input, including `"missing"`
1144 /// for absent declarations. With `cancellation = None`, this is one
1145 /// Lean-side bulk dispatch over an `Array String`; with
1146 /// `Some(token)`, this loops through the singular path so the token
1147 /// can be checked between items.
1148 ///
1149 /// # Errors
1150 ///
1151 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side
1152 /// bulk shim raises through `IO`.
1153 pub fn declaration_kind_bulk(
1154 &mut self,
1155 names: &[&str],
1156 cancellation: Option<&LeanCancellationToken>,
1157 progress: Option<&dyn LeanProgressSink>,
1158 ) -> LeanResult<Vec<String>> {
1159 let _span = tracing::debug_span!(
1160 target: "lean_rs",
1161 "lean_rs.host.session.declaration_kind_bulk",
1162 batch_size = names.len(),
1163 )
1164 .entered();
1165 if names.is_empty() {
1166 return Ok(Vec::new());
1167 }
1168 check_cancellation(cancellation)?;
1169 if cancellation.is_some() {
1170 let started = Instant::now();
1171 let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1172 let mut out = Vec::with_capacity(names.len());
1173 for (idx, name) in names.iter().enumerate() {
1174 check_cancellation(cancellation)?;
1175 out.push(self.declaration_kind(name, cancellation)?);
1176 report_progress(
1177 progress,
1178 "declaration_kind_bulk",
1179 u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
1180 total,
1181 started,
1182 )?;
1183 }
1184 return Ok(out);
1185 }
1186 if progress.is_none()
1187 && let Some(name) = Self::all_equal_name(names)
1188 {
1189 let names_owned = vec![name.to_owned()];
1190 let t = Instant::now();
1191 let mut result = Self::decode_strings_cached(
1192 self.shims
1193 .env_declaration_kind_bulk
1194 .call(self.environment.clone(), names_owned)?,
1195 )?;
1196 let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1197 self.record_call(batch_len, t.elapsed());
1198 let value = result.pop().unwrap_or_default();
1199 return Ok(vec![value; names.len()]);
1200 }
1201 let names_owned: Vec<String> = names.iter().map(|&name| name.to_owned()).collect();
1202 if let Some(sink) = progress {
1203 let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1204 let bridge = ProgressBridge::new(sink, "declaration_kind_bulk", total)?;
1205 let (handle, trampoline) = bridge.abi_parts();
1206 let t = Instant::now();
1207 let result = self.shims.env_declaration_kind_bulk_progress.call(
1208 self.environment.clone(),
1209 names_owned,
1210 handle,
1211 trampoline,
1212 );
1213 let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1214 self.record_call(batch_len, t.elapsed());
1215 let raw = bridge.decode::<Vec<Obj<'lean>>>(result?)?;
1216 Self::decode_strings_cached(raw)
1217 } else {
1218 let t = Instant::now();
1219 let result = self
1220 .shims
1221 .env_declaration_kind_bulk
1222 .call(self.environment.clone(), names_owned);
1223 let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1224 self.record_call(batch_len, t.elapsed());
1225 Self::decode_strings_cached(result?)
1226 }
1227 }
1228
1229 /// The Lean-rendered display string of `name`. Round-trips a name
1230 /// through the capability's `Name.toString` shim so callers see the
1231 /// same canonical form Lean would log.
1232 ///
1233 /// Diagnostic only—not a semantic key. Use
1234 /// [`LeanSession::query_declaration`] + a typed handle when
1235 /// equality matters.
1236 ///
1237 /// # Errors
1238 ///
1239 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side query
1240 /// raises.
1241 pub fn declaration_name(&mut self, name: &str, cancellation: Option<&LeanCancellationToken>) -> LeanResult<String> {
1242 let _span = tracing::debug_span!(
1243 target: "lean_rs",
1244 "lean_rs.host.session.declaration_name",
1245 name = name,
1246 )
1247 .entered();
1248 check_cancellation(cancellation)?;
1249 let name_handle = self.make_name(name, cancellation)?;
1250 check_cancellation(cancellation)?;
1251 let t = Instant::now();
1252 let result = self
1253 .shims
1254 .env_declaration_name
1255 .call(self.environment.clone(), name_handle);
1256 self.record_call(0, t.elapsed());
1257 result
1258 }
1259
1260 /// Lean-rendered display strings for `names`, preserving input
1261 /// order.
1262 ///
1263 /// This is diagnostic text, not a semantic key. Missing
1264 /// declarations are not an error because the singular
1265 /// [`Self::declaration_name`] path also only round-trips the input
1266 /// name through Lean's `Name.toString` renderer.
1267 ///
1268 /// With `cancellation = None`, this is one Lean-side bulk dispatch
1269 /// over an `Array String`; with `Some(token)`, this loops through
1270 /// the singular path so the token can be checked between items.
1271 ///
1272 /// # Errors
1273 ///
1274 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side
1275 /// bulk shim raises through `IO`.
1276 pub fn declaration_name_bulk(
1277 &mut self,
1278 names: &[&str],
1279 cancellation: Option<&LeanCancellationToken>,
1280 progress: Option<&dyn LeanProgressSink>,
1281 ) -> LeanResult<Vec<String>> {
1282 let _span = tracing::debug_span!(
1283 target: "lean_rs",
1284 "lean_rs.host.session.declaration_name_bulk",
1285 batch_size = names.len(),
1286 )
1287 .entered();
1288 if names.is_empty() {
1289 return Ok(Vec::new());
1290 }
1291 check_cancellation(cancellation)?;
1292 if cancellation.is_some() {
1293 let started = Instant::now();
1294 let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1295 let mut out = Vec::with_capacity(names.len());
1296 for (idx, name) in names.iter().enumerate() {
1297 check_cancellation(cancellation)?;
1298 out.push(self.declaration_name(name, cancellation)?);
1299 report_progress(
1300 progress,
1301 "declaration_name_bulk",
1302 u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
1303 total,
1304 started,
1305 )?;
1306 }
1307 return Ok(out);
1308 }
1309 if progress.is_none()
1310 && let Some(name) = Self::all_equal_name(names)
1311 {
1312 let names_owned = vec![name.to_owned()];
1313 let t = Instant::now();
1314 let mut result = Self::decode_strings_cached(
1315 self.shims
1316 .env_declaration_name_bulk
1317 .call(self.environment.clone(), names_owned)?,
1318 )?;
1319 let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1320 self.record_call(batch_len, t.elapsed());
1321 let value = result.pop().unwrap_or_default();
1322 return Ok(vec![value; names.len()]);
1323 }
1324 let names_owned: Vec<String> = names.iter().map(|&name| name.to_owned()).collect();
1325 if let Some(sink) = progress {
1326 let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1327 let bridge = ProgressBridge::new(sink, "declaration_name_bulk", total)?;
1328 let (handle, trampoline) = bridge.abi_parts();
1329 let t = Instant::now();
1330 let result = self.shims.env_declaration_name_bulk_progress.call(
1331 self.environment.clone(),
1332 names_owned,
1333 handle,
1334 trampoline,
1335 );
1336 let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1337 self.record_call(batch_len, t.elapsed());
1338 let raw = bridge.decode::<Vec<Obj<'lean>>>(result?)?;
1339 Self::decode_strings_cached(raw)
1340 } else {
1341 let t = Instant::now();
1342 let result = self
1343 .shims
1344 .env_declaration_name_bulk
1345 .call(self.environment.clone(), names_owned);
1346 let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1347 self.record_call(batch_len, t.elapsed());
1348 Self::decode_strings_cached(result?)
1349 }
1350 }
1351
1352 /// Render an opaque [`LeanName`] handle as its dotted-string form,
1353 /// routed through the capability's `Name.toString` shim.
1354 ///
1355 /// This is the supported way to turn a `LeanName` (e.g. an element
1356 /// of [`Self::list_declarations_filtered`]'s result) into Rust text.
1357 /// The output is diagnostic—not a semantic key—and equality on
1358 /// the underlying `Lean.Name` still lives in Lean.
1359 ///
1360 /// # Errors
1361 ///
1362 /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
1363 /// already cancelled before dispatch.
1364 pub fn name_to_string(
1365 &mut self,
1366 name: &LeanName<'lean>,
1367 cancellation: Option<&LeanCancellationToken>,
1368 ) -> LeanResult<String> {
1369 let _span = tracing::debug_span!(target: "lean_rs", "lean_rs.host.session.name_to_string").entered();
1370 check_cancellation(cancellation)?;
1371 let t = Instant::now();
1372 let result = self.shims.name_to_string.call(name.clone());
1373 self.record_call(0, t.elapsed());
1374 result
1375 }
1376
1377 /// Render `names` as dotted-string forms, preserving input order.
1378 ///
1379 /// Implemented as a per-item loop over [`Self::name_to_string`] in
1380 /// v1: cancellation is checked between items, progress is reported
1381 /// after each. The Lean shim is pure and short, so the per-item FFI
1382 /// overhead is acceptable; a bulk shim is a future optimisation if
1383 /// profiling shows it matters.
1384 ///
1385 /// # Errors
1386 ///
1387 /// Returns [`lean_rs::LeanError::Cancelled`] between items if the
1388 /// token is tripped during the walk.
1389 pub fn name_to_string_bulk(
1390 &mut self,
1391 names: &[LeanName<'lean>],
1392 cancellation: Option<&LeanCancellationToken>,
1393 progress: Option<&dyn LeanProgressSink>,
1394 ) -> LeanResult<Vec<String>> {
1395 let _span = tracing::debug_span!(
1396 target: "lean_rs",
1397 "lean_rs.host.session.name_to_string_bulk",
1398 batch_size = names.len(),
1399 )
1400 .entered();
1401 if names.is_empty() {
1402 return Ok(Vec::new());
1403 }
1404 check_cancellation(cancellation)?;
1405 let started = Instant::now();
1406 let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1407 let mut out = Vec::with_capacity(names.len());
1408 for (idx, name) in names.iter().enumerate() {
1409 check_cancellation(cancellation)?;
1410 out.push(self.name_to_string(name, cancellation)?);
1411 report_progress(
1412 progress,
1413 "name_to_string_bulk",
1414 u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
1415 total,
1416 started,
1417 )?;
1418 }
1419 Ok(out)
1420 }
1421
1422 /// Enumerate the imported environment's declaration names and render
1423 /// each as a dotted string. Convenience over
1424 /// [`Self::list_declarations_filtered`] + [`Self::name_to_string_bulk`]
1425 /// for the common case where the consumer only needs strings.
1426 ///
1427 /// Two FFI hops (list + per-name render) and one heap allocation
1428 /// per name. For batches under a few thousand this is fine; for
1429 /// six-figure walks consider the lower-level pair so the listing
1430 /// pass and the rendering pass can be cancelled or chunked
1431 /// independently.
1432 ///
1433 /// # Errors
1434 ///
1435 /// Forwards errors from [`Self::list_declarations_filtered`] and
1436 /// [`Self::name_to_string_bulk`].
1437 pub fn list_declarations_strings(
1438 &mut self,
1439 filter: &LeanDeclarationFilter,
1440 cancellation: Option<&LeanCancellationToken>,
1441 progress: Option<&dyn LeanProgressSink>,
1442 ) -> LeanResult<Vec<String>> {
1443 let _span = tracing::debug_span!(target: "lean_rs", "lean_rs.host.session.list_declarations_strings").entered();
1444 let names = self.list_declarations_filtered(filter, cancellation, None)?;
1445 self.name_to_string_bulk(&names, cancellation, progress)
1446 }
1447
1448 /// Search declarations with structural filters and bounded metadata rows.
1449 ///
1450 /// The search runs inside Lean while traversing the imported environment.
1451 /// It may inspect declaration types structurally for required constants and
1452 /// conclusion heads, but it never renders type text. Use
1453 /// [`Self::declaration_type`] for explicit one-name type rendering.
1454 ///
1455 /// # Errors
1456 ///
1457 /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is already
1458 /// cancelled before dispatch. Returns [`lean_rs::LeanError::LeanException`]
1459 /// if the Lean-side search raises.
1460 pub fn search_declarations(
1461 &mut self,
1462 search: &DeclarationSearchRequest,
1463 cancellation: Option<&LeanCancellationToken>,
1464 ) -> LeanResult<DeclarationSearchResult> {
1465 let _span = tracing::debug_span!(
1466 target: "lean_rs",
1467 "lean_rs.host.session.search_declarations",
1468 limit = search.limit,
1469 include_source = search.include_source,
1470 )
1471 .entered();
1472 check_cancellation(cancellation)?;
1473 let source_roots = if search.include_source {
1474 self.capabilities
1475 .host()
1476 .project()
1477 .source_roots()?
1478 .into_iter()
1479 .map(|path| path.to_string_lossy().into_owned())
1480 .collect::<Vec<_>>()
1481 } else {
1482 Vec::new()
1483 };
1484 check_cancellation(cancellation)?;
1485 let t = Instant::now();
1486 let result = self
1487 .shims
1488 .env_search_declarations
1489 .call(self.environment.clone(), search.clone(), source_roots);
1490 self.record_call(0, t.elapsed());
1491 result
1492 }
1493
1494 /// Inspect one selected declaration under explicit output budgets.
1495 ///
1496 /// Search remains metadata-only; callers use this method after selecting
1497 /// one declaration name whose rendered statement/docstring are worth
1498 /// paying for. Missing names and missing optional shim support are normal
1499 /// result statuses.
1500 ///
1501 /// # Errors
1502 ///
1503 /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is already
1504 /// cancelled before dispatch. Returns [`lean_rs::LeanError::LeanException`]
1505 /// if the Lean-side inspection raises.
1506 pub fn inspect_declaration(
1507 &mut self,
1508 request: &DeclarationInspectionRequest,
1509 cancellation: Option<&LeanCancellationToken>,
1510 ) -> LeanResult<DeclarationInspectionResult> {
1511 let _span = tracing::debug_span!(
1512 target: "lean_rs",
1513 "lean_rs.host.session.inspect_declaration",
1514 source = request.fields.source,
1515 statement = request.fields.statement,
1516 docstring = request.fields.docstring,
1517 attributes = request.fields.attributes,
1518 flags = request.fields.flags,
1519 )
1520 .entered();
1521 check_cancellation(cancellation)?;
1522 let Some(inspect) = &self.shims.env_inspect_declaration else {
1523 return Ok(DeclarationInspectionResult::Unsupported);
1524 };
1525 let source_roots = if request.fields.source {
1526 self.capabilities
1527 .host()
1528 .project()
1529 .source_roots()?
1530 .into_iter()
1531 .map(|path| path.to_string_lossy().into_owned())
1532 .collect::<Vec<_>>()
1533 } else {
1534 Vec::new()
1535 };
1536 check_cancellation(cancellation)?;
1537 let t = Instant::now();
1538 // Bound the optional notation-aware statement rendering by the default
1539 // heartbeat budget; a deeply nested term that exceeds it falls back to
1540 // the raw `Expr.toString` form inside the shim.
1541 let result = inspect.call(
1542 self.environment.clone(),
1543 request.clone(),
1544 source_roots,
1545 lean_toolchain::LEAN_HEARTBEAT_LIMIT_DEFAULT,
1546 );
1547 self.record_call(0, t.elapsed());
1548 result
1549 }
1550
1551 /// Render `expr` via `Expr.toString`—the cheap, deterministic
1552 /// projection.
1553 ///
1554 /// Walks the syntax tree directly: no `MetaM`, no notation lookup,
1555 /// no binder pretty-printing. The result is a legible-but-ugly
1556 /// dump suitable for indexing, logging, and search keys. For the
1557 /// form a Lean user reads, use the optional
1558 /// [`crate::host::meta::pp_expr`] service through
1559 /// [`Self::run_meta`] instead—it pays for elaboration context to
1560 /// get notation and unfolding right but can time out under a tight
1561 /// heartbeat budget.
1562 ///
1563 /// # Errors
1564 ///
1565 /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
1566 /// already cancelled before dispatch.
1567 pub fn expr_to_string_raw(
1568 &mut self,
1569 expr: &LeanExpr<'lean>,
1570 cancellation: Option<&LeanCancellationToken>,
1571 ) -> LeanResult<String> {
1572 let _span = tracing::debug_span!(target: "lean_rs", "lean_rs.host.session.expr_to_string_raw").entered();
1573 check_cancellation(cancellation)?;
1574 let t = Instant::now();
1575 let result = self.shims.env_expr_to_string_raw.call(expr.clone());
1576 self.record_call(0, t.elapsed());
1577 result
1578 }
1579
1580 /// Parse and elaborate a Lean module, returning only the requested
1581 /// bounded projection.
1582 ///
1583 /// The Lean shim owns header parsing, module-system header handling,
1584 /// info-tree traversal, cursor selection, reference collection, and
1585 /// bounded expression/goal rendering. The Rust side chooses a
1586 /// [`ModuleQuery`] and receives the matching
1587 /// [`ModuleQueryOutcome`]; whole-file raw expression/type dumps never
1588 /// cross this boundary.
1589 ///
1590 /// The shim is optional. When the loaded capability dylib does not
1591 /// export `lean_rs_host_process_module_query`, the method returns
1592 /// [`ModuleQueryOutcome::Unsupported`] without an FFI call.
1593 ///
1594 /// # Errors
1595 ///
1596 /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
1597 /// already cancelled before dispatch. Returns
1598 /// [`lean_rs::LeanError::LeanException`] if the Lean-side shim
1599 /// raises through `IO`. Returns [`lean_rs::LeanError::Host`] with
1600 /// stage [`HostStage::Conversion`] if the Lean return value does
1601 /// not decode into [`ModuleQueryOutcome`].
1602 pub fn process_module_query(
1603 &mut self,
1604 source: &str,
1605 query: &ModuleQuery,
1606 options: &LeanElabOptions,
1607 cancellation: Option<&LeanCancellationToken>,
1608 ) -> LeanResult<ModuleQueryOutcome> {
1609 let _span = tracing::debug_span!(
1610 target: "lean_rs",
1611 "lean_rs.host.session.process_module_query",
1612 source_len = source.len(),
1613 heartbeats = options.heartbeats(),
1614 diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1615 )
1616 .entered();
1617 check_cancellation(cancellation)?;
1618 let Some(call) = self.shims.process_module_query.as_ref() else {
1619 return Ok(ModuleQueryOutcome::Unsupported);
1620 };
1621 let t = Instant::now();
1622 let result = call.call(
1623 self.environment.clone(),
1624 source.to_owned(),
1625 query.clone(),
1626 options.namespace_context_str().to_owned(),
1627 options.file_label_str().to_owned(),
1628 options.heartbeats(),
1629 options.diagnostic_byte_limit_usize(),
1630 );
1631 self.record_call(0, t.elapsed());
1632 result
1633 }
1634
1635 /// Parse and elaborate a Lean module once, returning several bounded
1636 /// projections keyed by selector id.
1637 ///
1638 /// This is the proof-agent path: Lean owns header handling, one body
1639 /// elaboration, info-tree traversal, and selector projection. Rust sends
1640 /// a small selector array and receives per-selector outcomes; whole-file
1641 /// info-tree arrays never cross the boundary.
1642 ///
1643 /// The shim is optional. When the loaded capability dylib does not
1644 /// export `lean_rs_host_process_module_query_batch`, the method returns
1645 /// [`ModuleQueryBatchOutcome::Unsupported`] without an FFI call.
1646 ///
1647 /// # Errors
1648 ///
1649 /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
1650 /// already cancelled before dispatch. Returns
1651 /// [`lean_rs::LeanError::LeanException`] if the Lean-side shim raises
1652 /// through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
1653 /// [`HostStage::Conversion`] if the Lean return value does not decode
1654 /// into [`ModuleQueryBatchOutcome`].
1655 pub fn process_module_query_batch(
1656 &mut self,
1657 source: &str,
1658 selectors: &[ModuleQuerySelector],
1659 budgets: &ModuleQueryOutputBudgets,
1660 options: &LeanElabOptions,
1661 cancellation: Option<&LeanCancellationToken>,
1662 ) -> LeanResult<ModuleQueryBatchOutcome> {
1663 let _span = tracing::debug_span!(
1664 target: "lean_rs",
1665 "lean_rs.host.session.process_module_query_batch",
1666 source_len = source.len(),
1667 selectors = selectors.len(),
1668 per_field_bytes = budgets.per_field_bytes,
1669 total_bytes = budgets.total_bytes,
1670 heartbeats = options.heartbeats(),
1671 diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1672 )
1673 .entered();
1674 check_cancellation(cancellation)?;
1675 let Some(call) = self.shims.process_module_query_batch.as_ref() else {
1676 return Ok(ModuleQueryBatchOutcome::Unsupported);
1677 };
1678 let selectors_owned = selectors.to_vec();
1679 let t = Instant::now();
1680 let result = call.call(
1681 self.environment.clone(),
1682 source.to_owned(),
1683 selectors_owned,
1684 budgets.clone(),
1685 options.namespace_context_str().to_owned(),
1686 options.file_label_str().to_owned(),
1687 options.heartbeats(),
1688 options.diagnostic_byte_limit_usize(),
1689 );
1690 self.record_call(u64::try_from(selectors.len()).unwrap_or(u64::MAX), t.elapsed());
1691 result
1692 }
1693
1694 /// Parse/elaborate a Lean module through the shim-owned module snapshot
1695 /// cache, then return bounded selector projections plus cache facts.
1696 ///
1697 /// The snapshot cache remains private to the loaded shim. Rust provides
1698 /// the stable cache key and conservative policy, but never receives raw
1699 /// info trees.
1700 ///
1701 /// # Errors
1702 ///
1703 /// Returns an error if cancellation is already requested, if the shim
1704 /// raises an `IO` exception, or if the Lean result cannot be decoded into
1705 /// the expected cached batch outcome.
1706 pub fn process_module_query_batch_cached(
1707 &mut self,
1708 source: &str,
1709 selectors: &[ModuleQuerySelector],
1710 budgets: &ModuleQueryOutputBudgets,
1711 options: &LeanElabOptions,
1712 policy: &ModuleQueryCachePolicy,
1713 cancellation: Option<&LeanCancellationToken>,
1714 ) -> LeanResult<ModuleQueryBatchCachedOutcome> {
1715 let _span = tracing::debug_span!(
1716 target: "lean_rs",
1717 "lean_rs.host.session.process_module_query_batch_cached",
1718 source_len = source.len(),
1719 selectors = selectors.len(),
1720 per_field_bytes = budgets.per_field_bytes,
1721 total_bytes = budgets.total_bytes,
1722 heartbeats = options.heartbeats(),
1723 diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1724 )
1725 .entered();
1726 check_cancellation(cancellation)?;
1727 let Some(call) = self.shims.process_module_query_batch_cached.as_ref() else {
1728 return Ok(ModuleQueryBatchCachedOutcome::Unsupported);
1729 };
1730 let selectors_owned = selectors.to_vec();
1731 let policy_text = format!(
1732 "{}\n{}\n{}\n{}\n{}",
1733 policy.file_identity, policy.key, policy.max_entries, policy.ttl_millis, policy.max_bytes
1734 );
1735 let t = Instant::now();
1736 let result = call.call(
1737 self.environment.clone(),
1738 source.to_owned(),
1739 selectors_owned,
1740 budgets.clone(),
1741 options.namespace_context_str().to_owned(),
1742 options.file_label_str().to_owned(),
1743 options.heartbeats(),
1744 options.diagnostic_byte_limit_usize(),
1745 policy_text,
1746 );
1747 self.record_call(u64::try_from(selectors.len()).unwrap_or(u64::MAX), t.elapsed());
1748 result
1749 }
1750
1751 /// Try proof snippets against an in-memory source overlay.
1752 ///
1753 /// The shim is optional. When the loaded capability dylib does not export
1754 /// `lean_rs_host_attempt_proof`, the method returns
1755 /// [`ProofAttemptOutcome::Unsupported`] without an FFI call.
1756 ///
1757 /// # Errors
1758 ///
1759 /// Returns an error if cancellation is already requested, if the shim
1760 /// raises an `IO` exception, or if the Lean result cannot be decoded.
1761 pub fn attempt_proof(
1762 &mut self,
1763 request: &ProofAttemptRequest,
1764 options: &LeanElabOptions,
1765 cancellation: Option<&LeanCancellationToken>,
1766 ) -> LeanResult<ProofAttemptOutcome> {
1767 let _span = tracing::debug_span!(
1768 target: "lean_rs",
1769 "lean_rs.host.session.attempt_proof",
1770 source_len = request.source.len(),
1771 candidates = request.candidates.len(),
1772 per_field_bytes = request.budgets.per_field_bytes,
1773 total_bytes = request.budgets.total_bytes,
1774 heartbeats = options.heartbeats(),
1775 diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1776 )
1777 .entered();
1778 check_cancellation(cancellation)?;
1779 let Some(call) = self.shims.attempt_proof.as_ref() else {
1780 return Ok(ProofAttemptOutcome::Unsupported);
1781 };
1782 let t = Instant::now();
1783 let result = call.call(
1784 self.environment.clone(),
1785 request.clone(),
1786 options.namespace_context_str().to_owned(),
1787 options.file_label_str().to_owned(),
1788 options.heartbeats(),
1789 options.diagnostic_byte_limit_usize(),
1790 );
1791 self.record_call(u64::try_from(request.candidates.len()).unwrap_or(u64::MAX), t.elapsed());
1792 result
1793 }
1794
1795 /// Verify one declaration in an in-memory source snapshot.
1796 ///
1797 /// The shim is optional. When the loaded capability dylib does not export
1798 /// `lean_rs_host_verify_declaration`, the method returns
1799 /// [`DeclarationVerificationOutcome::Unsupported`] without an FFI call.
1800 ///
1801 /// # Errors
1802 ///
1803 /// Returns an error if cancellation is already requested, if the shim
1804 /// raises an `IO` exception, or if the Lean result cannot be decoded.
1805 pub fn verify_declaration(
1806 &mut self,
1807 request: &DeclarationVerificationRequest,
1808 options: &LeanElabOptions,
1809 cancellation: Option<&LeanCancellationToken>,
1810 ) -> LeanResult<DeclarationVerificationOutcome> {
1811 let _span = tracing::debug_span!(
1812 target: "lean_rs",
1813 "lean_rs.host.session.verify_declaration",
1814 source_len = request.source.len(),
1815 report_axioms = request.report_axioms,
1816 per_field_bytes = request.budgets.per_field_bytes,
1817 total_bytes = request.budgets.total_bytes,
1818 heartbeats = options.heartbeats(),
1819 diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1820 )
1821 .entered();
1822 check_cancellation(cancellation)?;
1823 let Some(call) = self.shims.verify_declaration.as_ref() else {
1824 return Ok(DeclarationVerificationOutcome::Unsupported);
1825 };
1826 let t = Instant::now();
1827 let result = call.call(
1828 self.environment.clone(),
1829 request.clone(),
1830 options.namespace_context_str().to_owned(),
1831 options.file_label_str().to_owned(),
1832 options.heartbeats(),
1833 options.diagnostic_byte_limit_usize(),
1834 );
1835 self.record_call(1, t.elapsed());
1836 result
1837 }
1838
1839 /// Verify several declarations in one in-memory source snapshot.
1840 ///
1841 /// The shim is optional. When the loaded capability dylib does not export
1842 /// `lean_rs_host_verify_declaration_batch`, the method returns
1843 /// [`DeclarationVerificationBatchOutcome::Unsupported`] without an FFI
1844 /// call. The returned rows preserve request order.
1845 ///
1846 /// # Errors
1847 ///
1848 /// Returns an error if cancellation is already requested, if the shim
1849 /// raises an `IO` exception, or if the Lean result cannot be decoded.
1850 pub fn verify_declaration_batch(
1851 &mut self,
1852 request: &DeclarationVerificationBatchRequest,
1853 options: &LeanElabOptions,
1854 cancellation: Option<&LeanCancellationToken>,
1855 ) -> LeanResult<DeclarationVerificationBatchOutcome> {
1856 let _span = tracing::debug_span!(
1857 target: "lean_rs",
1858 "lean_rs.host.session.verify_declaration_batch",
1859 source_len = request.source.len(),
1860 targets = request.targets.len(),
1861 report_axioms = request.report_axioms,
1862 per_field_bytes = request.budgets.per_field_bytes,
1863 total_bytes = request.budgets.total_bytes,
1864 heartbeats = options.heartbeats(),
1865 diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1866 )
1867 .entered();
1868 check_cancellation(cancellation)?;
1869 let Some(call) = self.shims.verify_declaration_batch.as_ref() else {
1870 return Ok(DeclarationVerificationBatchOutcome::Unsupported);
1871 };
1872 let t = Instant::now();
1873 let result = call.call(
1874 self.environment.clone(),
1875 request.clone(),
1876 options.namespace_context_str().to_owned(),
1877 options.file_label_str().to_owned(),
1878 options.heartbeats(),
1879 options.diagnostic_byte_limit_usize(),
1880 );
1881 self.record_call(u64::try_from(request.targets.len()).unwrap_or(u64::MAX), t.elapsed());
1882 result
1883 }
1884
1885 /// Clear the shim-owned module snapshot cache when the loaded capability
1886 /// supports it.
1887 ///
1888 /// This releases only cached module snapshots built by the bundled
1889 /// info-tree shim. It does not reset Lean's runtime, unload imported
1890 /// modules, or make full-session compacted regions safe to free.
1891 ///
1892 /// # Errors
1893 ///
1894 /// Returns an error if the shim raises an `IO` exception or if the Lean
1895 /// clear result cannot be decoded.
1896 pub fn clear_module_snapshot_cache(&mut self) -> LeanResult<ModuleSnapshotCacheClearResult> {
1897 let Some(call) = self.shims.clear_module_snapshot_cache.as_ref() else {
1898 return Ok(ModuleSnapshotCacheClearResult {
1899 entries_cleared: 0,
1900 approx_bytes_cleared: 0,
1901 });
1902 };
1903 let t = Instant::now();
1904 let result = call.call();
1905 self.record_call(0, t.elapsed());
1906 result
1907 }
1908
1909 /// Parse and elaborate a single Lean term against the imported
1910 /// environment, optionally against an expected type.
1911 ///
1912 /// The boundary is explicit: Rust supplies the source text, module
1913 /// context, and bounded options; Lean parses, elaborates, and
1914 /// returns either an opaque [`LeanExpr`] handle or a structured
1915 /// [`LeanElabFailure`] carrying typed diagnostics. Rust does not
1916 /// inspect elaborator internals or proof terms to decide
1917 /// correctness.
1918 ///
1919 /// The outer [`LeanResult`] surfaces host-stack failures (a Lean
1920 /// `IO`-level exception from the shim itself, a malformed Lean
1921 /// return value); the inner `Result` distinguishes successful
1922 /// elaboration from parse / type / kernel-stage failures the
1923 /// elaborator reports through its `MessageLog`. Both error paths
1924 /// propagate the [`LeanElabOptions::diagnostic_byte_limit`] bound
1925 /// structurally.
1926 ///
1927 /// # Errors
1928 ///
1929 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side shim raises
1930 /// through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
1931 /// [`HostStage::Conversion`] if the Lean return value does not
1932 /// decode into [`LeanElabFailure`] / [`LeanExpr`].
1933 pub fn elaborate(
1934 &mut self,
1935 source: &str,
1936 expected_type: Option<&LeanExpr<'lean>>,
1937 options: &LeanElabOptions,
1938 cancellation: Option<&LeanCancellationToken>,
1939 ) -> LeanResult<Result<LeanExpr<'lean>, LeanElabFailure>> {
1940 let _span = tracing::debug_span!(
1941 target: "lean_rs",
1942 "lean_rs.host.session.elaborate",
1943 source_len = source.len(),
1944 heartbeats = options.heartbeats(),
1945 diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1946 )
1947 .entered();
1948 check_cancellation(cancellation)?;
1949 let t = Instant::now();
1950 let result = self.shims.elaborate.call(
1951 self.environment.clone(),
1952 source.to_owned(),
1953 expected_type.cloned(),
1954 options.namespace_context_str().to_owned(),
1955 options.file_label_str().to_owned(),
1956 options.heartbeats(),
1957 options.diagnostic_byte_limit_usize(),
1958 );
1959 self.record_call(0, t.elapsed());
1960 result
1961 }
1962
1963 /// Parse, elaborate, and kernel-check a Lean declaration source
1964 /// (typically a `theorem` or `def`), returning a typed outcome
1965 /// that classifies the result and carries either the produced
1966 /// [`crate::LeanEvidence`] handle or the diagnostics the elaborator and
1967 /// kernel emitted.
1968 ///
1969 /// The boundary is explicit (mirrors [`Self::elaborate`]): Rust
1970 /// supplies source + options; Lean parses, elaborates, runs
1971 /// `addDecl` (which kernel-checks), and classifies the outcome.
1972 /// Rust never inspects the produced proof term or declaration
1973 /// internals.
1974 ///
1975 /// # Errors
1976 ///
1977 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side shim
1978 /// raises through `IO` (an unexpected internal failure that is not
1979 /// itself a rejection / unavailable diagnostic). Returns
1980 /// [`lean_rs::LeanError::Host`] with stage [`HostStage::Conversion`] if the
1981 /// Lean return value does not decode into [`LeanKernelOutcome`].
1982 pub fn kernel_check(
1983 &mut self,
1984 source: &str,
1985 options: &LeanElabOptions,
1986 cancellation: Option<&LeanCancellationToken>,
1987 progress: Option<&dyn LeanProgressSink>,
1988 ) -> LeanResult<LeanKernelOutcome<'lean>> {
1989 let _span = tracing::debug_span!(
1990 target: "lean_rs",
1991 "lean_rs.host.session.kernel_check",
1992 source_len = source.len(),
1993 heartbeats = options.heartbeats(),
1994 diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1995 )
1996 .entered();
1997 check_cancellation(cancellation)?;
1998 if let Some(sink) = progress {
1999 let bridge = ProgressBridge::new(sink, "kernel_check", Some(1))?;
2000 let (handle, trampoline) = bridge.abi_parts();
2001 let t = Instant::now();
2002 let result = self.shims.kernel_check_progress.call(
2003 self.environment.clone(),
2004 source.to_owned(),
2005 options.namespace_context_str().to_owned(),
2006 options.file_label_str().to_owned(),
2007 options.heartbeats(),
2008 options.diagnostic_byte_limit_usize(),
2009 handle,
2010 trampoline,
2011 );
2012 self.record_call(0, t.elapsed());
2013 bridge.decode(result?)
2014 } else {
2015 let t = Instant::now();
2016 let result = self.shims.kernel_check.call(
2017 self.environment.clone(),
2018 source.to_owned(),
2019 options.namespace_context_str().to_owned(),
2020 options.file_label_str().to_owned(),
2021 options.heartbeats(),
2022 options.diagnostic_byte_limit_usize(),
2023 );
2024 self.record_call(0, t.elapsed());
2025 result
2026 }
2027 }
2028
2029 /// Re-validate a previously captured [`LeanEvidence`] against the
2030 /// session's imported environment, returning the kernel's current
2031 /// verdict.
2032 ///
2033 /// The handle was produced by an earlier
2034 /// [`Self::kernel_check`] call against this same environment and
2035 /// carries the kernel-accepted `Lean.Declaration` opaquely. The
2036 /// session never installs that declaration into its stored
2037 /// environment, so re-checking against the unchanged environment
2038 /// is the supported way to ask "is this evidence still valid?"—
2039 /// the kernel runs fresh.
2040 ///
2041 /// The returned [`EvidenceStatus`] mirrors
2042 /// [`LeanKernelOutcome::status`]: `Checked` on success, `Rejected`
2043 /// if the kernel now refuses the declaration, `Unavailable` if
2044 /// the Lean shim caught an `IO` exception. The Lean fixture does
2045 /// not currently emit `Unsupported` from this path—`Unsupported`
2046 /// only fires during the initial classification in
2047 /// `kernel_check`.
2048 ///
2049 /// # Errors
2050 ///
2051 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean shim raises
2052 /// through `IO` outside of its own `try` (an unexpected internal
2053 /// failure that the shim did not classify). Returns
2054 /// [`lean_rs::LeanError::Host`] with stage [`HostStage::Conversion`] if the
2055 /// return value does not decode as a four-tag
2056 /// [`EvidenceStatus`] inductive.
2057 pub fn check_evidence(
2058 &mut self,
2059 handle: &LeanEvidence<'lean>,
2060 cancellation: Option<&LeanCancellationToken>,
2061 ) -> LeanResult<EvidenceStatus> {
2062 let _span = tracing::debug_span!(
2063 target: "lean_rs",
2064 "lean_rs.host.session.check_evidence",
2065 )
2066 .entered();
2067 check_cancellation(cancellation)?;
2068 let t = Instant::now();
2069 let result = self.shims.check_evidence.call(self.environment.clone(), handle.clone());
2070 self.record_call(0, t.elapsed());
2071 result
2072 }
2073
2074 /// Project a previously captured [`LeanEvidence`] into a bounded
2075 /// [`ProofSummary`] for diagnostics or storage.
2076 ///
2077 /// The Lean shim renders the captured declaration's name, kind,
2078 /// and type expression as three byte-bounded `String`s—no
2079 /// `Lean.Expr` or proof term crosses the FFI boundary. The
2080 /// summary is computed on demand (not at
2081 /// [`Self::kernel_check`] time) because most callers only ever
2082 /// inspect the [`EvidenceStatus`] tag and would pay the
2083 /// pretty-print cost for nothing.
2084 ///
2085 /// Strings on the returned summary are display text. They are not
2086 /// semantic keys; route equality comparisons through a
2087 /// Lean-authored equality export.
2088 ///
2089 /// # Errors
2090 ///
2091 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean shim raises
2092 /// through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
2093 /// [`HostStage::Conversion`] if the return value does not decode
2094 /// as a three-field [`ProofSummary`] structure.
2095 pub fn summarize_evidence(
2096 &mut self,
2097 handle: &LeanEvidence<'lean>,
2098 cancellation: Option<&LeanCancellationToken>,
2099 ) -> LeanResult<ProofSummary> {
2100 let _span = tracing::debug_span!(
2101 target: "lean_rs",
2102 "lean_rs.host.session.summarize_evidence",
2103 )
2104 .entered();
2105 check_cancellation(cancellation)?;
2106 let t = Instant::now();
2107 let result = self
2108 .shims
2109 .evidence_summary
2110 .call(self.environment.clone(), handle.clone());
2111 self.record_call(0, t.elapsed());
2112 result
2113 }
2114
2115 /// Invoke a registered bounded [`MetaM`](https://leanprover.github.io/theorem_proving_in_lean4/)
2116 /// service against the imported environment.
2117 ///
2118 /// The session dispatches through the checked binding for the closed
2119 /// service shape; if the loaded capability does not export the optional
2120 /// symbol, the call short-circuits to [`LeanMetaResponse::Unsupported`]
2121 /// with a synthetic host-side diagnostic naming the missing symbol.
2122 ///
2123 /// The outer [`LeanResult`] surfaces host-stack failures (a Lean
2124 /// `IO`-level exception from the shim itself, or an undecodable
2125 /// return value). The four-way classification—`Ok` / `Failed` /
2126 /// `TimeoutOrHeartbeat` / `Unsupported`—lives in the inner
2127 /// [`LeanMetaResponse`].
2128 ///
2129 /// # Errors
2130 ///
2131 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean shim raises
2132 /// through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
2133 /// [`HostStage::Conversion`] if the return value does not decode
2134 /// into [`LeanMetaResponse<Resp>`].
2135 pub fn run_meta<Req, Resp>(
2136 &mut self,
2137 service: &LeanMetaService<Req, Resp>,
2138 request: Req,
2139 options: &LeanMetaOptions,
2140 cancellation: Option<&LeanCancellationToken>,
2141 ) -> LeanResult<LeanMetaResponse<Resp>>
2142 where
2143 LeanMetaService<Req, Resp>: HostMetaDispatch<'lean, Req, Resp>,
2144 Req: lean_rs::abi::traits::LeanAbi<'lean>,
2145 Resp: TryFromLean<'lean>,
2146 {
2147 let _span = tracing::debug_span!(
2148 target: "lean_rs",
2149 "lean_rs.host.session.run_meta",
2150 service = service.name(),
2151 heartbeats = options.heartbeats(),
2152 diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
2153 )
2154 .entered();
2155 check_cancellation(cancellation)?;
2156 service.dispatch(self, request, options)
2157 }
2158
2159 /// Look up many declarations in one Lean traversal.
2160 ///
2161 /// Equivalent to calling [`Self::query_declaration`] in a loop over
2162 /// `names`, except that the entire batch crosses the FFI boundary
2163 /// exactly once: one `Array Name` allocation in, one
2164 /// `Array (Option Declaration)` allocation out. The Lean shim folds
2165 /// the singular `envQueryDeclaration` across the input array, so the
2166 /// iteration semantics are identical to a Rust-side fold over the
2167 /// singular path—a missing name still errors the batch.
2168 ///
2169 /// Names are still resolved through the capability's
2170 /// `name_from_string` shim, one [`lean_rs::LeanName`] handle per
2171 /// input. The metric impact is `names.len() + 1` recorded FFI calls
2172 /// for a batch of `names.len()` items, versus `2 * names.len()` for
2173 /// the same workload through [`Self::query_declaration`].
2174 ///
2175 /// # Errors
2176 ///
2177 /// Returns [`lean_rs::LeanError::Host`] with stage [`HostStage::Conversion`]
2178 /// on the first name that is not present in the imported
2179 /// environment, with the missing name in the diagnostic. Returns
2180 /// [`lean_rs::LeanError::LeanException`] if the Lean-side bulk shim raises
2181 /// through `IO`.
2182 pub fn query_declarations_bulk(
2183 &mut self,
2184 names: &[&str],
2185 cancellation: Option<&LeanCancellationToken>,
2186 progress: Option<&dyn LeanProgressSink>,
2187 ) -> LeanResult<Vec<LeanDeclaration<'lean>>> {
2188 let _span = tracing::debug_span!(
2189 target: "lean_rs",
2190 "lean_rs.host.session.query_declarations_bulk",
2191 batch_size = names.len(),
2192 )
2193 .entered();
2194 if names.is_empty() {
2195 return Ok(Vec::new());
2196 }
2197 check_cancellation(cancellation)?;
2198 if cancellation.is_some() {
2199 let started = Instant::now();
2200 let mut out = Vec::with_capacity(names.len());
2201 let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
2202 for (idx, name) in names.iter().enumerate() {
2203 check_cancellation(cancellation)?;
2204 out.push(self.query_declaration(name, cancellation)?);
2205 report_progress(
2206 progress,
2207 "query_declarations_bulk",
2208 u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
2209 total,
2210 started,
2211 )?;
2212 }
2213 return Ok(out);
2214 }
2215 let prepare_started = Instant::now();
2216 let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
2217 let mut name_handles: Vec<LeanName<'lean>> = Vec::with_capacity(names.len());
2218 for (idx, name) in names.iter().enumerate() {
2219 name_handles.push(self.make_name(name, cancellation)?);
2220 report_progress(
2221 progress,
2222 "prepare_names",
2223 u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
2224 total,
2225 prepare_started,
2226 )?;
2227 }
2228 check_cancellation(cancellation)?;
2229 let raw = if let Some(sink) = progress {
2230 let bridge = ProgressBridge::new(sink, "query_declarations_bulk", total)?;
2231 let (handle, trampoline) = bridge.abi_parts();
2232 let t = Instant::now();
2233 let result = self.shims.env_query_declarations_bulk_progress.call(
2234 self.environment.clone(),
2235 name_handles,
2236 handle,
2237 trampoline,
2238 );
2239 let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
2240 self.record_call(batch_len, t.elapsed());
2241 bridge.decode::<Vec<Option<LeanDeclaration<'lean>>>>(result?)?
2242 } else {
2243 let t = Instant::now();
2244 let result = self
2245 .shims
2246 .env_query_declarations_bulk
2247 .call(self.environment.clone(), name_handles);
2248 let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
2249 self.record_call(batch_len, t.elapsed());
2250 result?
2251 };
2252 let mut out: Vec<LeanDeclaration<'lean>> = Vec::with_capacity(raw.len());
2253 for (slot, name) in raw.into_iter().zip(names.iter()) {
2254 match slot {
2255 Some(decl) => out.push(decl),
2256 None => {
2257 return Err(lean_rs::abi::traits::conversion_error(format!(
2258 "declaration '{name}' not found in imported environment"
2259 )));
2260 }
2261 }
2262 }
2263 Ok(out)
2264 }
2265
2266 /// Parse and elaborate many independent Lean terms in one Lean
2267 /// traversal.
2268 ///
2269 /// Per-source `Result<LeanExpr, LeanElabFailure>` shape matches
2270 /// [`Self::elaborate`] exactly: outer [`LeanResult`] surfaces
2271 /// host-stack failures, inner per-source `Result` distinguishes
2272 /// successful elaboration from elaborator-reported diagnostics. A
2273 /// caller treating the bulk path as a fold over the singular path
2274 /// sees no semantic surprise.
2275 ///
2276 /// The `expected_type` parameter is **not** carried by the bulk
2277 /// shape: per-source expectations would force a parallel
2278 /// `&[Option<&LeanExpr>]` array, and no in-tree caller has earned
2279 /// the surface. Use [`Self::elaborate`] for individual terms with
2280 /// expected types.
2281 ///
2282 /// The heartbeat and diagnostic-byte budgets in `options` apply
2283 /// once each per source (the Lean shim builds fresh
2284 /// [`Lean.Options`](https://leanprover.github.io/) per item via the
2285 /// same `hostElaborate` path), so the per-batch upper bound on
2286 /// elapsed CPU work is `sources.len() * options.heartbeats()`.
2287 ///
2288 /// # Errors
2289 ///
2290 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side bulk shim
2291 /// raises through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
2292 /// [`HostStage::Conversion`] if the Lean return value does not
2293 /// decode into a `Vec<Result<LeanExpr, LeanElabFailure>>`.
2294 pub fn elaborate_bulk(
2295 &mut self,
2296 sources: &[&str],
2297 options: &LeanElabOptions,
2298 cancellation: Option<&LeanCancellationToken>,
2299 progress: Option<&dyn LeanProgressSink>,
2300 ) -> LeanResult<Vec<Result<LeanExpr<'lean>, LeanElabFailure>>> {
2301 let _span = tracing::debug_span!(
2302 target: "lean_rs",
2303 "lean_rs.host.session.elaborate_bulk",
2304 batch_size = sources.len(),
2305 heartbeats = options.heartbeats(),
2306 diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
2307 )
2308 .entered();
2309 if sources.is_empty() {
2310 return Ok(Vec::new());
2311 }
2312 check_cancellation(cancellation)?;
2313 if cancellation.is_some() {
2314 let started = Instant::now();
2315 let total = Some(u64::try_from(sources.len()).unwrap_or(u64::MAX));
2316 let mut out = Vec::with_capacity(sources.len());
2317 for (idx, source) in sources.iter().enumerate() {
2318 check_cancellation(cancellation)?;
2319 out.push(self.elaborate(source, None, options, cancellation)?);
2320 report_progress(
2321 progress,
2322 "elaborate_bulk",
2323 u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
2324 total,
2325 started,
2326 )?;
2327 }
2328 return Ok(out);
2329 }
2330 let sources_owned: Vec<String> = sources.iter().map(|&s| s.to_owned()).collect();
2331 if let Some(sink) = progress {
2332 let total = Some(u64::try_from(sources.len()).unwrap_or(u64::MAX));
2333 let bridge = ProgressBridge::new(sink, "elaborate_bulk", total)?;
2334 let (handle, trampoline) = bridge.abi_parts();
2335 let t = Instant::now();
2336 let result = self.shims.elaborate_bulk_progress.call(
2337 self.environment.clone(),
2338 sources_owned,
2339 options.namespace_context_str().to_owned(),
2340 options.file_label_str().to_owned(),
2341 options.heartbeats(),
2342 options.diagnostic_byte_limit_usize(),
2343 handle,
2344 trampoline,
2345 );
2346 let batch_len = u64::try_from(sources.len()).unwrap_or(u64::MAX);
2347 self.record_call(batch_len, t.elapsed());
2348 bridge.decode(result?)
2349 } else {
2350 let t = Instant::now();
2351 let result = self.shims.elaborate_bulk.call(
2352 self.environment.clone(),
2353 sources_owned,
2354 options.namespace_context_str().to_owned(),
2355 options.file_label_str().to_owned(),
2356 options.heartbeats(),
2357 options.diagnostic_byte_limit_usize(),
2358 );
2359 let batch_len = u64::try_from(sources.len()).unwrap_or(u64::MAX);
2360 self.record_call(batch_len, t.elapsed());
2361 result
2362 }
2363 }
2364
2365 /// Build a `LeanName` from a dotted Rust string via the capability's
2366 /// `Name.toName` shim.
2367 fn make_name(&self, name: &str, cancellation: Option<&LeanCancellationToken>) -> LeanResult<LeanName<'lean>> {
2368 check_cancellation(cancellation)?;
2369 let lean_name = lean_rs::__host_internals::string_from_str(self.capabilities.host().runtime(), name);
2370 let t = Instant::now();
2371 let result = self.shims.name_from_string.call(lean_name);
2372 self.record_call(0, t.elapsed());
2373 result
2374 }
2375}
2376
2377trait HostMetaDispatch<'lean, Req, Resp> {
2378 fn dispatch(
2379 &self,
2380 session: &mut LeanSession<'lean, '_>,
2381 request: Req,
2382 options: &LeanMetaOptions,
2383 ) -> LeanResult<LeanMetaResponse<Resp>>;
2384}
2385
2386impl<'lean> HostMetaDispatch<'lean, LeanExpr<'lean>, LeanExpr<'lean>>
2387 for LeanMetaService<LeanExpr<'lean>, LeanExpr<'lean>>
2388{
2389 fn dispatch(
2390 &self,
2391 session: &mut LeanSession<'lean, '_>,
2392 request: LeanExpr<'lean>,
2393 options: &LeanMetaOptions,
2394 ) -> LeanResult<LeanMetaResponse<LeanExpr<'lean>>> {
2395 let Some(call) = (match self.name() {
2396 "lean_rs_host_meta_infer_type" => session.shims.meta_infer_type.as_ref(),
2397 "lean_rs_host_meta_whnf" => session.shims.meta_whnf.as_ref(),
2398 "lean_rs_host_meta_heartbeat_burn" => session.shims.meta_heartbeat_burn.as_ref(),
2399 _ => None,
2400 }) else {
2401 return Ok(unsupported_meta_response(self.name()));
2402 };
2403 let t = Instant::now();
2404 let result = call.call(
2405 session.environment.clone(),
2406 request,
2407 options.heartbeats(),
2408 options.diagnostic_byte_limit_usize(),
2409 options.transparency_byte(),
2410 );
2411 session.record_call(0, t.elapsed());
2412 result
2413 }
2414}
2415
2416impl<'lean>
2417 HostMetaDispatch<
2418 'lean,
2419 (
2420 LeanExpr<'lean>,
2421 LeanExpr<'lean>,
2422 crate::host::meta::LeanMetaTransparency,
2423 ),
2424 bool,
2425 >
2426 for LeanMetaService<
2427 (
2428 LeanExpr<'lean>,
2429 LeanExpr<'lean>,
2430 crate::host::meta::LeanMetaTransparency,
2431 ),
2432 bool,
2433 >
2434{
2435 fn dispatch(
2436 &self,
2437 session: &mut LeanSession<'lean, '_>,
2438 request: (
2439 LeanExpr<'lean>,
2440 LeanExpr<'lean>,
2441 crate::host::meta::LeanMetaTransparency,
2442 ),
2443 options: &LeanMetaOptions,
2444 ) -> LeanResult<LeanMetaResponse<bool>> {
2445 let Some(call) = session.shims.meta_is_def_eq.as_ref() else {
2446 return Ok(unsupported_meta_response(self.name()));
2447 };
2448 let t = Instant::now();
2449 let result = call.call(
2450 session.environment.clone(),
2451 request,
2452 options.heartbeats(),
2453 options.diagnostic_byte_limit_usize(),
2454 options.transparency_byte(),
2455 );
2456 session.record_call(0, t.elapsed());
2457 result
2458 }
2459}
2460
2461impl<'lean> HostMetaDispatch<'lean, LeanExpr<'lean>, String> for LeanMetaService<LeanExpr<'lean>, String> {
2462 fn dispatch(
2463 &self,
2464 session: &mut LeanSession<'lean, '_>,
2465 request: LeanExpr<'lean>,
2466 options: &LeanMetaOptions,
2467 ) -> LeanResult<LeanMetaResponse<String>> {
2468 let Some(call) = session.shims.meta_pp_expr.as_ref() else {
2469 return Ok(unsupported_meta_response(self.name()));
2470 };
2471 let t = Instant::now();
2472 let result = call.call(
2473 session.environment.clone(),
2474 request,
2475 options.heartbeats(),
2476 options.diagnostic_byte_limit_usize(),
2477 options.transparency_byte(),
2478 );
2479 session.record_call(0, t.elapsed());
2480 result
2481 }
2482}
2483
2484fn unsupported_meta_response<Resp>(symbol: &str) -> LeanMetaResponse<Resp> {
2485 LeanMetaResponse::Unsupported(LeanElabFailure::synthetic(
2486 format!("bundled host shim does not export meta service '{symbol}'"),
2487 "<lean-rs-host meta>".to_owned(),
2488 ))
2489}