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 /// Value of the process-global extension-registration stamp at the moment
560 /// this session's environment was imported. See
561 /// [`Self::extension_registry_epoch`] for what a mismatch means.
562 extension_registry_epoch: u64,
563 /// Per-session dispatch metrics. `Cell` because every query method
564 /// takes `&mut self` but the bulk path can also be invoked through a
565 /// shared reference (e.g. inside a fold helper)—keeping the
566 /// counter in `Cell` makes the recording uniform without adding an
567 /// extra `&mut` borrow at each call site.
568 stats: Cell<SessionStats>,
569}
570
571/// Process-wide serialization for [`LeanSession::import`]. See the
572/// comment at the lock-acquire site for the Lean-4.30 race it closes.
573static SESSION_IMPORT_LOCK: Mutex<()> = Mutex::new(());
574
575pub(crate) fn with_session_import_lock<T>(f: impl FnOnce() -> LeanResult<T>) -> LeanResult<T> {
576 let _import_guard = SESSION_IMPORT_LOCK
577 .lock()
578 .unwrap_or_else(|poisoned| poisoned.into_inner());
579 f()
580}
581
582impl<'lean, 'c> LeanSession<'lean, 'c> {
583 pub(crate) fn import_search_paths(capabilities: &LeanCapabilities<'lean, '_>) -> LeanResult<Vec<String>> {
584 let project = capabilities.host().project();
585 let mut search_paths: Vec<String> = project
586 .olean_search_paths()
587 .into_iter()
588 .map(|path| path.to_string_lossy().into_owned())
589 .collect();
590 search_paths.push(
591 crate::host::lake::LakeProject::interop_olean_search_path()?
592 .to_string_lossy()
593 .into_owned(),
594 );
595 search_paths.push(
596 crate::host::lake::LakeProject::shim_olean_search_path()?
597 .to_string_lossy()
598 .into_owned(),
599 );
600 Ok(search_paths)
601 }
602
603 /// Import the named modules into a fresh Lean environment and wrap
604 /// it as a session.
605 ///
606 /// The Lean-side `lean_rs_host_session_import` receives the Lake
607 /// project root (so it can `Lean.initSearchPath` the `.olean`
608 /// directory) and the module-name list, and returns the resulting
609 /// environment. Failures surface as
610 /// [`lean_rs::LeanError::LeanException`] with the message Lean produced.
611 pub(crate) fn import(
612 capabilities: &'c LeanCapabilities<'lean, 'c>,
613 imports: &[&str],
614 cancellation: Option<&LeanCancellationToken>,
615 progress: Option<&dyn LeanProgressSink>,
616 ) -> LeanResult<Self> {
617 Self::import_with_profile(
618 capabilities,
619 imports,
620 LeanSessionImportProfile::default(),
621 cancellation,
622 progress,
623 )
624 }
625
626 pub(crate) fn import_with_profile(
627 capabilities: &'c LeanCapabilities<'lean, 'c>,
628 imports: &[&str],
629 profile: LeanSessionImportProfile,
630 cancellation: Option<&LeanCancellationToken>,
631 progress: Option<&dyn LeanProgressSink>,
632 ) -> LeanResult<Self> {
633 let _span = tracing::info_span!(
634 target: "lean_rs",
635 "lean_rs.host.session.import",
636 profile = profile.label(),
637 imports_len = imports.len(),
638 )
639 .entered();
640 check_cancellation(cancellation)?;
641 let search_paths = Self::import_search_paths(capabilities)?;
642 let imports_owned: Vec<String> = imports.iter().map(|&s| s.to_owned()).collect();
643 // Lean 4.30 strictly enforces `enableInitializersExecution` before
644 // `importModules (loadExts := true)`. The flag is process-global,
645 // but `Lean.withImporting` (wrapped around every import) resets it
646 // on completion—two threads importing concurrently race the
647 // shim's enable→import sequence and the loser sees the flag
648 // cleared by the winner's reset. Serializing the import phase
649 // across the process matches Lean's "single execution thread
650 // accessing the global references" requirement. Sessions operate
651 // concurrently on their own `Environment` values once import
652 // returns; the lock spans only the FFI call.
653 with_session_import_lock(|| {
654 let shims = HostShimBindings::resolve(capabilities.shim_capability())
655 .map_err(|err| binding_error_to_lean_error(&err))?;
656 let environment = if let Some(sink) = progress {
657 let bridge =
658 ProgressBridge::new(sink, "import", Some(u64::try_from(imports.len()).unwrap_or(u64::MAX)))?;
659 let (handle, trampoline) = bridge.abi_parts();
660 let raw = if profile == LeanSessionImportProfile::default() {
661 shims
662 .session_import_progress
663 .call(search_paths, imports_owned, handle, trampoline)?
664 } else {
665 shims.session_import_profile_progress.call(
666 search_paths,
667 imports_owned,
668 profile.import_all(),
669 profile.import_level().code(),
670 handle,
671 trampoline,
672 )?
673 };
674 bridge.decode(raw)?
675 } else if profile == LeanSessionImportProfile::default() {
676 shims.session_import.call(search_paths, imports_owned)?
677 } else {
678 shims.session_import_profile.call(
679 search_paths,
680 imports_owned,
681 profile.import_all(),
682 profile.import_level().code(),
683 profile.load_exts(),
684 false,
685 false,
686 String::new(),
687 )?
688 };
689 let import_stats = shims.env_import_stats.call(
690 environment.clone(),
691 profile.import_level().as_str().to_owned(),
692 profile.load_exts(),
693 )?;
694 // Read after the import, and inside the import lock, so the stamp
695 // describes the registry as it stood once this environment's own
696 // initializers had run. Reading it earlier would record a value
697 // this environment is already newer than.
698 let extension_registry_epoch = shims.extension_registry_epoch.call()?;
699 Ok(Self {
700 capabilities,
701 shims,
702 environment,
703 import_stats,
704 extension_registry_epoch,
705 stats: Cell::new(SessionStats::default()),
706 })
707 })
708 }
709
710 pub(crate) fn import_profiled(
711 capabilities: &'c LeanCapabilities<'lean, 'c>,
712 imports: &[&str],
713 mode: LeanImportProfileMode,
714 profiler_options: &LeanImportProfilerOptions,
715 ) -> LeanResult<Self> {
716 let _span = tracing::info_span!(
717 target: "lean_rs",
718 "lean_rs.host.session.import_profiled",
719 mode = mode.label(),
720 imports_len = imports.len(),
721 )
722 .entered();
723 let search_paths = Self::import_search_paths(capabilities)?;
724 let imports_owned: Vec<String> = imports.iter().map(|&s| s.to_owned()).collect();
725 with_session_import_lock(|| {
726 let shims = HostShimBindings::resolve(capabilities.shim_capability())
727 .map_err(|err| binding_error_to_lean_error(&err))?;
728 let environment = shims.session_import_profile.call(
729 search_paths,
730 imports_owned,
731 mode.import_all(),
732 mode.import_level().code(),
733 mode.load_exts(),
734 profiler_options.profiler,
735 profiler_options.trace_profiler,
736 profiler_options.trace_profiler_output.clone().unwrap_or_default(),
737 )?;
738 let import_stats = shims.env_import_stats.call(
739 environment.clone(),
740 mode.import_level().as_str().to_owned(),
741 mode.load_exts(),
742 )?;
743 let extension_registry_epoch = shims.extension_registry_epoch.call()?;
744 Ok(Self {
745 capabilities,
746 shims,
747 environment,
748 import_stats,
749 extension_registry_epoch,
750 stats: Cell::new(SessionStats::default()),
751 })
752 })
753 }
754
755 /// Wrap a previously-imported `Lean.Environment` as a fresh
756 /// [`LeanSession`] over `capabilities`.
757 ///
758 /// Crate-private; only [`crate::host::pool::SessionPool::acquire`]
759 /// calls this to recycle a pooled environment under a new
760 /// capability borrow. The returned session's [`SessionStats`] start
761 /// at zero—accumulated counters from the previous owner do not
762 /// leak across pool checkouts.
763 pub(crate) fn from_environment_with_import_stats(
764 capabilities: &'c LeanCapabilities<'lean, 'c>,
765 environment: Obj<'lean>,
766 import_stats: LeanImportStats,
767 extension_registry_epoch: u64,
768 ) -> LeanResult<Self> {
769 let shims = HostShimBindings::resolve(capabilities.shim_capability())
770 .map_err(|err| binding_error_to_lean_error(&err))?;
771 Ok(Self {
772 capabilities,
773 shims,
774 environment,
775 import_stats,
776 extension_registry_epoch,
777 stats: Cell::new(SessionStats::default()),
778 })
779 }
780
781 /// Consume the session and return its owned `Lean.Environment`.
782 ///
783 /// Crate-private; only [`crate::host::pool::SessionPool`] uses this
784 /// to reclaim the environment when a [`crate::host::pool::PooledSession`]
785 /// drops. The returned `Obj<'lean>` carries one Lean refcount, which
786 /// the pool is responsible for either pushing back into the free
787 /// list (in which case `Drop` runs later when the pool itself
788 /// drops) or releasing immediately (when at capacity).
789 pub(crate) fn into_environment(self) -> Obj<'lean> {
790 self.environment
791 }
792
793 /// Snapshot of this session's accumulated dispatch metrics.
794 ///
795 /// Returns a copy; the counters keep accumulating after the call.
796 /// Use [`SessionStats::default`] to compute a delta across two
797 /// snapshots.
798 #[must_use]
799 pub fn stats(&self) -> SessionStats {
800 self.stats.get()
801 }
802
803 /// Lean-native attribution for this session's imported environment.
804 #[must_use]
805 pub fn import_stats(&self) -> &LeanImportStats {
806 &self.import_stats
807 }
808
809 /// The process-global extension-registration stamp as it stood when this
810 /// session's environment was imported.
811 ///
812 /// Opaque and monotone; only equality against
813 /// [`Self::live_extension_registry_epoch`] carries meaning — never compare
814 /// two stamps with `<`. When the two differ, a later import registered a
815 /// Lean environment extension and this environment's `extensions` array is
816 /// permanently shorter than the registry: Lean sizes it once, at
817 /// `finalizeImport`, and keeps both the growth helper and the field
818 /// `private`. Elaborating any `namespace`, `section`, or `open … in`
819 /// against it makes `ScopedEnvExtension.pushScope` index past the end and
820 /// `panic!`. There is no repair — the only sound response is to drop the
821 /// session and re-import.
822 ///
823 /// A bare non-persistent `registerEnvExtension` moves none of the three
824 /// registries the stamp sums, so in principle it can grow the array
825 /// requirement without moving the stamp. The resulting mismatch is not
826 /// reachable: extension indices are shared across kinds, so if such an
827 /// extension lands at index *n* then `scopedEnvExtensionsRef` did not grow
828 /// and every scoped extension the blind iteration touches still has an
829 /// index inside the short array. The only other blind walks are over
830 /// `persistentEnvExtensionsRef` — import finalization, and olean writing,
831 /// which this host never performs. Beyond those, such an extension is
832 /// reached only through an explicit `ext.getState env` from the module that
833 /// registered it, and that module is not in the stale environment's import
834 /// closure. Re-check this argument at every toolchain bump.
835 #[must_use]
836 pub fn extension_registry_epoch(&self) -> u64 {
837 self.extension_registry_epoch
838 }
839
840 /// The current process-global extension-registration stamp.
841 ///
842 /// Compare against [`Self::extension_registry_epoch`] to decide whether
843 /// this session's environment is still safe to elaborate against. The
844 /// stamp is process-global, so one read answers for every live session,
845 /// whichever one it is read through.
846 ///
847 /// # Errors
848 ///
849 /// Returns an error if the shim raises through `IO` or the result cannot
850 /// be decoded.
851 pub fn live_extension_registry_epoch(&self) -> LeanResult<u64> {
852 let started = Instant::now();
853 let epoch = self.shims.extension_registry_epoch.call();
854 self.record_call(0, started.elapsed());
855 epoch
856 }
857
858 /// Internal helper: record one FFI call and add `batch` per-item
859 /// entries plus `elapsed` nanoseconds. Singular methods pass
860 /// `batch = 0`; bulk methods pass the input length.
861 fn record_call(&self, batch: u64, elapsed: std::time::Duration) {
862 let mut s = self.stats.get();
863 s.ffi_calls = s.ffi_calls.saturating_add(1);
864 s.batch_items = s.batch_items.saturating_add(batch);
865 s.elapsed_ns = s
866 .elapsed_ns
867 .saturating_add(u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX));
868 self.stats.set(s);
869 }
870
871 fn decode_strings_cached(raw: Vec<Obj<'lean>>) -> LeanResult<Vec<String>> {
872 if raw.is_empty() {
873 return Ok(Vec::new());
874 }
875 let Some(first_key) = raw.first().map(Obj::as_raw_borrowed) else {
876 return Ok(Vec::new());
877 };
878 if raw.iter().all(|obj| obj.as_raw_borrowed() == first_key) {
879 let len = raw.len();
880 let mut raw_iter = raw.into_iter();
881 let Some(first) = raw_iter.next() else {
882 return Ok(Vec::new());
883 };
884 let value = String::try_from_lean(first)?;
885 return Ok(vec![value; len]);
886 }
887 let mut out = Vec::with_capacity(raw.len());
888 for obj in raw {
889 out.push(String::try_from_lean(obj)?);
890 }
891 Ok(out)
892 }
893
894 fn all_equal_name<'a>(names: &'a [&str]) -> Option<&'a str> {
895 let first = *names.first()?;
896 names.iter().all(|name| *name == first).then_some(first)
897 }
898
899 /// Look up a declaration by full Lean name (e.g. `"Nat.zero"`).
900 ///
901 /// # Errors
902 ///
903 /// Returns [`lean_rs::LeanError::Host`] with stage [`HostStage::Conversion`]
904 /// if the name is not present in the imported environment. Returns
905 /// [`lean_rs::LeanError::LeanException`] if the Lean-side query raises.
906 pub fn query_declaration(
907 &mut self,
908 name: &str,
909 cancellation: Option<&LeanCancellationToken>,
910 ) -> LeanResult<LeanDeclaration<'lean>> {
911 let _span = tracing::debug_span!(
912 target: "lean_rs",
913 "lean_rs.host.session.query_declaration",
914 name = name,
915 )
916 .entered();
917 check_cancellation(cancellation)?;
918 let name_handle = self.make_name(name, cancellation)?;
919 check_cancellation(cancellation)?;
920 let t = Instant::now();
921 let result = self
922 .shims
923 .env_query_declaration
924 .call(self.environment.clone(), name_handle);
925 self.record_call(0, t.elapsed());
926 match result? {
927 Some(decl) => Ok(decl),
928 None => Err(lean_rs::abi::traits::conversion_error(format!(
929 "declaration '{name}' not found in imported environment"
930 ))),
931 }
932 }
933
934 /// All declaration names in the imported environment.
935 ///
936 /// Returns a Vec; the environment's `constants` map contains many
937 /// thousands of entries even for a small project (Lean prelude is
938 /// always imported), so prefer [`LeanSession::query_declaration`]
939 /// when you already know the name.
940 ///
941 /// # Errors
942 ///
943 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side query
944 /// raises.
945 pub fn list_declarations(
946 &mut self,
947 cancellation: Option<&LeanCancellationToken>,
948 ) -> LeanResult<Vec<LeanName<'lean>>> {
949 let _span = tracing::debug_span!(
950 target: "lean_rs",
951 "lean_rs.host.session.list_declarations",
952 )
953 .entered();
954 check_cancellation(cancellation)?;
955 let t = Instant::now();
956 let raw = self.shims.env_list_declarations.call(self.environment.clone());
957 self.record_call(0, t.elapsed());
958 raw?.into_iter().map(LeanName::try_from_lean).collect()
959 }
960
961 /// Declaration names matching `filter`.
962 ///
963 /// Filtering runs inside Lean while traversing the environment
964 /// constants table, so Rust only allocates handles for names the
965 /// caller asked to keep.
966 ///
967 /// # Errors
968 ///
969 /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
970 /// already cancelled before dispatch. Returns
971 /// [`lean_rs::LeanError::LeanException`] if the Lean-side query
972 /// raises.
973 pub fn list_declarations_filtered(
974 &mut self,
975 filter: &LeanDeclarationFilter,
976 cancellation: Option<&LeanCancellationToken>,
977 progress: Option<&dyn LeanProgressSink>,
978 ) -> LeanResult<Vec<LeanName<'lean>>> {
979 let _span = tracing::debug_span!(
980 target: "lean_rs",
981 "lean_rs.host.session.list_declarations_filtered",
982 include_private = filter.include_private,
983 include_generated = filter.include_generated,
984 include_internal = filter.include_internal,
985 )
986 .entered();
987 check_cancellation(cancellation)?;
988 let raw = if let Some(sink) = progress {
989 let bridge = ProgressBridge::new(sink, "list_declarations_filtered", None)?;
990 let (handle, trampoline) = bridge.abi_parts();
991 let t = Instant::now();
992 let result = self.shims.env_list_declarations_filtered_progress.call(
993 self.environment.clone(),
994 *filter,
995 handle,
996 trampoline,
997 );
998 self.record_call(0, t.elapsed());
999 bridge.decode::<Vec<Obj<'lean>>>(result?)?
1000 } else {
1001 let t = Instant::now();
1002 let result = self
1003 .shims
1004 .env_list_declarations_filtered
1005 .call(self.environment.clone(), *filter);
1006 self.record_call(0, t.elapsed());
1007 result?
1008 };
1009 raw.into_iter().map(LeanName::try_from_lean).collect()
1010 }
1011
1012 /// Source range Lean recorded for `name`.
1013 ///
1014 /// Returns `Ok(None)` when the name is absent or Lean has no
1015 /// declaration range for it. That is normal for synthetic,
1016 /// runtime-created, and some compiler-generated declarations.
1017 ///
1018 /// # Errors
1019 ///
1020 /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
1021 /// already cancelled before dispatch. Returns
1022 /// [`lean_rs::LeanError::LeanException`] if the Lean-side query
1023 /// raises.
1024 pub fn declaration_source_range(
1025 &mut self,
1026 name: &str,
1027 cancellation: Option<&LeanCancellationToken>,
1028 ) -> LeanResult<Option<LeanSourceRange>> {
1029 let _span = tracing::debug_span!(
1030 target: "lean_rs",
1031 "lean_rs.host.session.declaration_source_range",
1032 name = name,
1033 )
1034 .entered();
1035 check_cancellation(cancellation)?;
1036 let name_handle = self.make_name(name, cancellation)?;
1037 check_cancellation(cancellation)?;
1038 let source_roots = self
1039 .capabilities
1040 .host()
1041 .project()
1042 .source_roots()?
1043 .into_iter()
1044 .map(|path| path.to_string_lossy().into_owned())
1045 .collect::<Vec<_>>();
1046 check_cancellation(cancellation)?;
1047 let t = Instant::now();
1048 let result = self
1049 .shims
1050 .env_declaration_source_range
1051 .call(self.environment.clone(), name_handle, source_roots);
1052 self.record_call(0, t.elapsed());
1053 result
1054 }
1055
1056 /// The declared type of `name`, as an opaque [`LeanExpr`] handle.
1057 ///
1058 /// Returns `Ok(None)` if the name is not present in the environment.
1059 ///
1060 /// # Errors
1061 ///
1062 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side query
1063 /// raises.
1064 pub fn declaration_type(
1065 &mut self,
1066 name: &str,
1067 cancellation: Option<&LeanCancellationToken>,
1068 ) -> LeanResult<Option<LeanExpr<'lean>>> {
1069 let _span = tracing::debug_span!(
1070 target: "lean_rs",
1071 "lean_rs.host.session.declaration_type",
1072 name = name,
1073 )
1074 .entered();
1075 check_cancellation(cancellation)?;
1076 let name_handle = self.make_name(name, cancellation)?;
1077 check_cancellation(cancellation)?;
1078 let t = Instant::now();
1079 let result = self
1080 .shims
1081 .env_declaration_type
1082 .call(self.environment.clone(), name_handle);
1083 self.record_call(0, t.elapsed());
1084 result
1085 }
1086
1087 /// The declared types of `names`, preserving input order.
1088 ///
1089 /// Returns `None` in each slot whose name is not present in the
1090 /// environment. With `cancellation = None`, the whole batch crosses
1091 /// the FFI boundary once and Lean converts the input strings to
1092 /// names internally. With `Some(token)`, this loops through
1093 /// [`Self::declaration_type`] so cancellation can be observed
1094 /// between items; partial results are discarded when cancellation
1095 /// fires.
1096 ///
1097 /// # Errors
1098 ///
1099 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side
1100 /// bulk shim raises through `IO`.
1101 pub fn declaration_type_bulk(
1102 &mut self,
1103 names: &[&str],
1104 cancellation: Option<&LeanCancellationToken>,
1105 progress: Option<&dyn LeanProgressSink>,
1106 ) -> LeanResult<Vec<Option<LeanExpr<'lean>>>> {
1107 let _span = tracing::debug_span!(
1108 target: "lean_rs",
1109 "lean_rs.host.session.declaration_type_bulk",
1110 batch_size = names.len(),
1111 )
1112 .entered();
1113 if names.is_empty() {
1114 return Ok(Vec::new());
1115 }
1116 check_cancellation(cancellation)?;
1117 if cancellation.is_some() {
1118 let started = Instant::now();
1119 let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1120 let mut out = Vec::with_capacity(names.len());
1121 for (idx, name) in names.iter().enumerate() {
1122 check_cancellation(cancellation)?;
1123 out.push(self.declaration_type(name, cancellation)?);
1124 report_progress(
1125 progress,
1126 "declaration_type_bulk",
1127 u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
1128 total,
1129 started,
1130 )?;
1131 }
1132 return Ok(out);
1133 }
1134 if progress.is_none()
1135 && let Some(name) = Self::all_equal_name(names)
1136 {
1137 let names_owned = vec![name.to_owned()];
1138 let t = Instant::now();
1139 let mut result = self
1140 .shims
1141 .env_declaration_type_bulk
1142 .call(self.environment.clone(), names_owned)?;
1143 let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1144 self.record_call(batch_len, t.elapsed());
1145 let value = result.pop().unwrap_or(None);
1146 return Ok(vec![value; names.len()]);
1147 }
1148 let names_owned: Vec<String> = names.iter().map(|&name| name.to_owned()).collect();
1149 if let Some(sink) = progress {
1150 let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1151 let bridge = ProgressBridge::new(sink, "declaration_type_bulk", total)?;
1152 let (handle, trampoline) = bridge.abi_parts();
1153 let t = Instant::now();
1154 let result = self.shims.env_declaration_type_bulk_progress.call(
1155 self.environment.clone(),
1156 names_owned,
1157 handle,
1158 trampoline,
1159 );
1160 let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1161 self.record_call(batch_len, t.elapsed());
1162 bridge.decode(result?)
1163 } else {
1164 let t = Instant::now();
1165 let result = self
1166 .shims
1167 .env_declaration_type_bulk
1168 .call(self.environment.clone(), names_owned);
1169 let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1170 self.record_call(batch_len, t.elapsed());
1171 result
1172 }
1173 }
1174
1175 /// The kind of `name` as a Lean-rendered string
1176 /// (`"axiom"`, `"definition"`, `"theorem"`, `"opaque"`, `"quot"`,
1177 /// `"inductive"`, `"constructor"`, `"recursor"`), or `"missing"`
1178 /// if `name` is not in the environment.
1179 ///
1180 /// # Errors
1181 ///
1182 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side query
1183 /// raises.
1184 pub fn declaration_kind(&mut self, name: &str, cancellation: Option<&LeanCancellationToken>) -> LeanResult<String> {
1185 let _span = tracing::debug_span!(
1186 target: "lean_rs",
1187 "lean_rs.host.session.declaration_kind",
1188 name = name,
1189 )
1190 .entered();
1191 check_cancellation(cancellation)?;
1192 let name_handle = self.make_name(name, cancellation)?;
1193 check_cancellation(cancellation)?;
1194 let t = Instant::now();
1195 let result = self
1196 .shims
1197 .env_declaration_kind
1198 .call(self.environment.clone(), name_handle);
1199 self.record_call(0, t.elapsed());
1200 result
1201 }
1202
1203 /// The declaration kinds of `names`, preserving input order.
1204 ///
1205 /// Each output slot is the same string that [`Self::declaration_kind`]
1206 /// would return for the corresponding input, including `"missing"`
1207 /// for absent declarations. With `cancellation = None`, this is one
1208 /// Lean-side bulk dispatch over an `Array String`; with
1209 /// `Some(token)`, this loops through the singular path so the token
1210 /// can be checked between items.
1211 ///
1212 /// # Errors
1213 ///
1214 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side
1215 /// bulk shim raises through `IO`.
1216 pub fn declaration_kind_bulk(
1217 &mut self,
1218 names: &[&str],
1219 cancellation: Option<&LeanCancellationToken>,
1220 progress: Option<&dyn LeanProgressSink>,
1221 ) -> LeanResult<Vec<String>> {
1222 let _span = tracing::debug_span!(
1223 target: "lean_rs",
1224 "lean_rs.host.session.declaration_kind_bulk",
1225 batch_size = names.len(),
1226 )
1227 .entered();
1228 if names.is_empty() {
1229 return Ok(Vec::new());
1230 }
1231 check_cancellation(cancellation)?;
1232 if cancellation.is_some() {
1233 let started = Instant::now();
1234 let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1235 let mut out = Vec::with_capacity(names.len());
1236 for (idx, name) in names.iter().enumerate() {
1237 check_cancellation(cancellation)?;
1238 out.push(self.declaration_kind(name, cancellation)?);
1239 report_progress(
1240 progress,
1241 "declaration_kind_bulk",
1242 u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
1243 total,
1244 started,
1245 )?;
1246 }
1247 return Ok(out);
1248 }
1249 if progress.is_none()
1250 && let Some(name) = Self::all_equal_name(names)
1251 {
1252 let names_owned = vec![name.to_owned()];
1253 let t = Instant::now();
1254 let mut result = Self::decode_strings_cached(
1255 self.shims
1256 .env_declaration_kind_bulk
1257 .call(self.environment.clone(), names_owned)?,
1258 )?;
1259 let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1260 self.record_call(batch_len, t.elapsed());
1261 let value = result.pop().unwrap_or_default();
1262 return Ok(vec![value; names.len()]);
1263 }
1264 let names_owned: Vec<String> = names.iter().map(|&name| name.to_owned()).collect();
1265 if let Some(sink) = progress {
1266 let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1267 let bridge = ProgressBridge::new(sink, "declaration_kind_bulk", total)?;
1268 let (handle, trampoline) = bridge.abi_parts();
1269 let t = Instant::now();
1270 let result = self.shims.env_declaration_kind_bulk_progress.call(
1271 self.environment.clone(),
1272 names_owned,
1273 handle,
1274 trampoline,
1275 );
1276 let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1277 self.record_call(batch_len, t.elapsed());
1278 let raw = bridge.decode::<Vec<Obj<'lean>>>(result?)?;
1279 Self::decode_strings_cached(raw)
1280 } else {
1281 let t = Instant::now();
1282 let result = self
1283 .shims
1284 .env_declaration_kind_bulk
1285 .call(self.environment.clone(), names_owned);
1286 let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1287 self.record_call(batch_len, t.elapsed());
1288 Self::decode_strings_cached(result?)
1289 }
1290 }
1291
1292 /// The Lean-rendered display string of `name`. Round-trips a name
1293 /// through the capability's `Name.toString` shim so callers see the
1294 /// same canonical form Lean would log.
1295 ///
1296 /// Diagnostic only—not a semantic key. Use
1297 /// [`LeanSession::query_declaration`] + a typed handle when
1298 /// equality matters.
1299 ///
1300 /// # Errors
1301 ///
1302 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side query
1303 /// raises.
1304 pub fn declaration_name(&mut self, name: &str, cancellation: Option<&LeanCancellationToken>) -> LeanResult<String> {
1305 let _span = tracing::debug_span!(
1306 target: "lean_rs",
1307 "lean_rs.host.session.declaration_name",
1308 name = name,
1309 )
1310 .entered();
1311 check_cancellation(cancellation)?;
1312 let name_handle = self.make_name(name, cancellation)?;
1313 check_cancellation(cancellation)?;
1314 let t = Instant::now();
1315 let result = self
1316 .shims
1317 .env_declaration_name
1318 .call(self.environment.clone(), name_handle);
1319 self.record_call(0, t.elapsed());
1320 result
1321 }
1322
1323 /// Lean-rendered display strings for `names`, preserving input
1324 /// order.
1325 ///
1326 /// This is diagnostic text, not a semantic key. Missing
1327 /// declarations are not an error because the singular
1328 /// [`Self::declaration_name`] path also only round-trips the input
1329 /// name through Lean's `Name.toString` renderer.
1330 ///
1331 /// With `cancellation = None`, this is one Lean-side bulk dispatch
1332 /// over an `Array String`; with `Some(token)`, this loops through
1333 /// the singular path so the token can be checked between items.
1334 ///
1335 /// # Errors
1336 ///
1337 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side
1338 /// bulk shim raises through `IO`.
1339 pub fn declaration_name_bulk(
1340 &mut self,
1341 names: &[&str],
1342 cancellation: Option<&LeanCancellationToken>,
1343 progress: Option<&dyn LeanProgressSink>,
1344 ) -> LeanResult<Vec<String>> {
1345 let _span = tracing::debug_span!(
1346 target: "lean_rs",
1347 "lean_rs.host.session.declaration_name_bulk",
1348 batch_size = names.len(),
1349 )
1350 .entered();
1351 if names.is_empty() {
1352 return Ok(Vec::new());
1353 }
1354 check_cancellation(cancellation)?;
1355 if cancellation.is_some() {
1356 let started = Instant::now();
1357 let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1358 let mut out = Vec::with_capacity(names.len());
1359 for (idx, name) in names.iter().enumerate() {
1360 check_cancellation(cancellation)?;
1361 out.push(self.declaration_name(name, cancellation)?);
1362 report_progress(
1363 progress,
1364 "declaration_name_bulk",
1365 u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
1366 total,
1367 started,
1368 )?;
1369 }
1370 return Ok(out);
1371 }
1372 if progress.is_none()
1373 && let Some(name) = Self::all_equal_name(names)
1374 {
1375 let names_owned = vec![name.to_owned()];
1376 let t = Instant::now();
1377 let mut result = Self::decode_strings_cached(
1378 self.shims
1379 .env_declaration_name_bulk
1380 .call(self.environment.clone(), names_owned)?,
1381 )?;
1382 let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1383 self.record_call(batch_len, t.elapsed());
1384 let value = result.pop().unwrap_or_default();
1385 return Ok(vec![value; names.len()]);
1386 }
1387 let names_owned: Vec<String> = names.iter().map(|&name| name.to_owned()).collect();
1388 if let Some(sink) = progress {
1389 let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1390 let bridge = ProgressBridge::new(sink, "declaration_name_bulk", total)?;
1391 let (handle, trampoline) = bridge.abi_parts();
1392 let t = Instant::now();
1393 let result = self.shims.env_declaration_name_bulk_progress.call(
1394 self.environment.clone(),
1395 names_owned,
1396 handle,
1397 trampoline,
1398 );
1399 let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1400 self.record_call(batch_len, t.elapsed());
1401 let raw = bridge.decode::<Vec<Obj<'lean>>>(result?)?;
1402 Self::decode_strings_cached(raw)
1403 } else {
1404 let t = Instant::now();
1405 let result = self
1406 .shims
1407 .env_declaration_name_bulk
1408 .call(self.environment.clone(), names_owned);
1409 let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
1410 self.record_call(batch_len, t.elapsed());
1411 Self::decode_strings_cached(result?)
1412 }
1413 }
1414
1415 /// Render an opaque [`LeanName`] handle as its dotted-string form,
1416 /// routed through the capability's `Name.toString` shim.
1417 ///
1418 /// This is the supported way to turn a `LeanName` (e.g. an element
1419 /// of [`Self::list_declarations_filtered`]'s result) into Rust text.
1420 /// The output is diagnostic—not a semantic key—and equality on
1421 /// the underlying `Lean.Name` still lives in Lean.
1422 ///
1423 /// # Errors
1424 ///
1425 /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
1426 /// already cancelled before dispatch.
1427 pub fn name_to_string(
1428 &mut self,
1429 name: &LeanName<'lean>,
1430 cancellation: Option<&LeanCancellationToken>,
1431 ) -> LeanResult<String> {
1432 let _span = tracing::debug_span!(target: "lean_rs", "lean_rs.host.session.name_to_string").entered();
1433 check_cancellation(cancellation)?;
1434 let t = Instant::now();
1435 let result = self.shims.name_to_string.call(name.clone());
1436 self.record_call(0, t.elapsed());
1437 result
1438 }
1439
1440 /// Render `names` as dotted-string forms, preserving input order.
1441 ///
1442 /// Implemented as a per-item loop over [`Self::name_to_string`] in
1443 /// v1: cancellation is checked between items, progress is reported
1444 /// after each. The Lean shim is pure and short, so the per-item FFI
1445 /// overhead is acceptable; a bulk shim is a future optimisation if
1446 /// profiling shows it matters.
1447 ///
1448 /// # Errors
1449 ///
1450 /// Returns [`lean_rs::LeanError::Cancelled`] between items if the
1451 /// token is tripped during the walk.
1452 pub fn name_to_string_bulk(
1453 &mut self,
1454 names: &[LeanName<'lean>],
1455 cancellation: Option<&LeanCancellationToken>,
1456 progress: Option<&dyn LeanProgressSink>,
1457 ) -> LeanResult<Vec<String>> {
1458 let _span = tracing::debug_span!(
1459 target: "lean_rs",
1460 "lean_rs.host.session.name_to_string_bulk",
1461 batch_size = names.len(),
1462 )
1463 .entered();
1464 if names.is_empty() {
1465 return Ok(Vec::new());
1466 }
1467 check_cancellation(cancellation)?;
1468 let started = Instant::now();
1469 let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
1470 let mut out = Vec::with_capacity(names.len());
1471 for (idx, name) in names.iter().enumerate() {
1472 check_cancellation(cancellation)?;
1473 out.push(self.name_to_string(name, cancellation)?);
1474 report_progress(
1475 progress,
1476 "name_to_string_bulk",
1477 u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
1478 total,
1479 started,
1480 )?;
1481 }
1482 Ok(out)
1483 }
1484
1485 /// Enumerate the imported environment's declaration names and render
1486 /// each as a dotted string. Convenience over
1487 /// [`Self::list_declarations_filtered`] + [`Self::name_to_string_bulk`]
1488 /// for the common case where the consumer only needs strings.
1489 ///
1490 /// Two FFI hops (list + per-name render) and one heap allocation
1491 /// per name. For batches under a few thousand this is fine; for
1492 /// six-figure walks consider the lower-level pair so the listing
1493 /// pass and the rendering pass can be cancelled or chunked
1494 /// independently.
1495 ///
1496 /// # Errors
1497 ///
1498 /// Forwards errors from [`Self::list_declarations_filtered`] and
1499 /// [`Self::name_to_string_bulk`].
1500 pub fn list_declarations_strings(
1501 &mut self,
1502 filter: &LeanDeclarationFilter,
1503 cancellation: Option<&LeanCancellationToken>,
1504 progress: Option<&dyn LeanProgressSink>,
1505 ) -> LeanResult<Vec<String>> {
1506 let _span = tracing::debug_span!(target: "lean_rs", "lean_rs.host.session.list_declarations_strings").entered();
1507 let names = self.list_declarations_filtered(filter, cancellation, None)?;
1508 self.name_to_string_bulk(&names, cancellation, progress)
1509 }
1510
1511 /// Search declarations with structural filters and bounded metadata rows.
1512 ///
1513 /// The search runs inside Lean while traversing the imported environment.
1514 /// It may inspect declaration types structurally for required constants and
1515 /// conclusion heads, but it never renders type text. Use
1516 /// [`Self::declaration_type`] for explicit one-name type rendering.
1517 ///
1518 /// # Errors
1519 ///
1520 /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is already
1521 /// cancelled before dispatch. Returns [`lean_rs::LeanError::LeanException`]
1522 /// if the Lean-side search raises.
1523 pub fn search_declarations(
1524 &mut self,
1525 search: &DeclarationSearchRequest,
1526 cancellation: Option<&LeanCancellationToken>,
1527 ) -> LeanResult<DeclarationSearchResult> {
1528 let _span = tracing::debug_span!(
1529 target: "lean_rs",
1530 "lean_rs.host.session.search_declarations",
1531 limit = search.limit,
1532 include_source = search.include_source,
1533 )
1534 .entered();
1535 check_cancellation(cancellation)?;
1536 let source_roots = if search.include_source {
1537 self.capabilities
1538 .host()
1539 .project()
1540 .source_roots()?
1541 .into_iter()
1542 .map(|path| path.to_string_lossy().into_owned())
1543 .collect::<Vec<_>>()
1544 } else {
1545 Vec::new()
1546 };
1547 check_cancellation(cancellation)?;
1548 let t = Instant::now();
1549 let result = self
1550 .shims
1551 .env_search_declarations
1552 .call(self.environment.clone(), search.clone(), source_roots);
1553 self.record_call(0, t.elapsed());
1554 result
1555 }
1556
1557 /// Inspect one selected declaration under explicit output budgets.
1558 ///
1559 /// Search remains metadata-only; callers use this method after selecting
1560 /// one declaration name whose rendered statement/docstring are worth
1561 /// paying for. Missing names and missing optional shim support are normal
1562 /// result statuses.
1563 ///
1564 /// # Errors
1565 ///
1566 /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is already
1567 /// cancelled before dispatch. Returns [`lean_rs::LeanError::LeanException`]
1568 /// if the Lean-side inspection raises.
1569 pub fn inspect_declaration(
1570 &mut self,
1571 request: &DeclarationInspectionRequest,
1572 cancellation: Option<&LeanCancellationToken>,
1573 ) -> LeanResult<DeclarationInspectionResult> {
1574 let _span = tracing::debug_span!(
1575 target: "lean_rs",
1576 "lean_rs.host.session.inspect_declaration",
1577 source = request.fields.source,
1578 statement = request.fields.statement,
1579 docstring = request.fields.docstring,
1580 attributes = request.fields.attributes,
1581 flags = request.fields.flags,
1582 )
1583 .entered();
1584 check_cancellation(cancellation)?;
1585 let Some(inspect) = &self.shims.env_inspect_declaration else {
1586 return Ok(DeclarationInspectionResult::Unsupported);
1587 };
1588 let source_roots = if request.fields.source {
1589 self.capabilities
1590 .host()
1591 .project()
1592 .source_roots()?
1593 .into_iter()
1594 .map(|path| path.to_string_lossy().into_owned())
1595 .collect::<Vec<_>>()
1596 } else {
1597 Vec::new()
1598 };
1599 check_cancellation(cancellation)?;
1600 let t = Instant::now();
1601 // Bound the optional notation-aware statement rendering by the default
1602 // heartbeat budget; a deeply nested term that exceeds it falls back to
1603 // the raw `Expr.toString` form inside the shim.
1604 let result = inspect.call(
1605 self.environment.clone(),
1606 request.clone(),
1607 source_roots,
1608 lean_toolchain::LEAN_HEARTBEAT_LIMIT_DEFAULT,
1609 );
1610 self.record_call(0, t.elapsed());
1611 result
1612 }
1613
1614 /// Render `expr` via `Expr.toString`—the cheap, deterministic
1615 /// projection.
1616 ///
1617 /// Walks the syntax tree directly: no `MetaM`, no notation lookup,
1618 /// no binder pretty-printing. The result is a legible-but-ugly
1619 /// dump suitable for indexing, logging, and search keys. For the
1620 /// form a Lean user reads, use the optional
1621 /// [`crate::host::meta::pp_expr`] service through
1622 /// [`Self::run_meta`] instead—it pays for elaboration context to
1623 /// get notation and unfolding right but can time out under a tight
1624 /// heartbeat budget.
1625 ///
1626 /// # Errors
1627 ///
1628 /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
1629 /// already cancelled before dispatch.
1630 pub fn expr_to_string_raw(
1631 &mut self,
1632 expr: &LeanExpr<'lean>,
1633 cancellation: Option<&LeanCancellationToken>,
1634 ) -> LeanResult<String> {
1635 let _span = tracing::debug_span!(target: "lean_rs", "lean_rs.host.session.expr_to_string_raw").entered();
1636 check_cancellation(cancellation)?;
1637 let t = Instant::now();
1638 let result = self.shims.env_expr_to_string_raw.call(expr.clone());
1639 self.record_call(0, t.elapsed());
1640 result
1641 }
1642
1643 /// Parse and elaborate a Lean module, returning only the requested
1644 /// bounded projection.
1645 ///
1646 /// The Lean shim owns header parsing, module-system header handling,
1647 /// info-tree traversal, cursor selection, reference collection, and
1648 /// bounded expression/goal rendering. The Rust side chooses a
1649 /// [`ModuleQuery`] and receives the matching
1650 /// [`ModuleQueryOutcome`]; whole-file raw expression/type dumps never
1651 /// cross this boundary.
1652 ///
1653 /// The shim is optional. When the loaded capability dylib does not
1654 /// export `lean_rs_host_process_module_query`, the method returns
1655 /// [`ModuleQueryOutcome::Unsupported`] without an FFI call.
1656 ///
1657 /// # Errors
1658 ///
1659 /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
1660 /// already cancelled before dispatch. Returns
1661 /// [`lean_rs::LeanError::LeanException`] if the Lean-side shim
1662 /// raises through `IO`. Returns [`lean_rs::LeanError::Host`] with
1663 /// stage [`HostStage::Conversion`] if the Lean return value does
1664 /// not decode into [`ModuleQueryOutcome`].
1665 pub fn process_module_query(
1666 &mut self,
1667 source: &str,
1668 query: &ModuleQuery,
1669 options: &LeanElabOptions,
1670 cancellation: Option<&LeanCancellationToken>,
1671 ) -> LeanResult<ModuleQueryOutcome> {
1672 let _span = tracing::debug_span!(
1673 target: "lean_rs",
1674 "lean_rs.host.session.process_module_query",
1675 source_len = source.len(),
1676 heartbeats = options.heartbeats(),
1677 diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1678 )
1679 .entered();
1680 check_cancellation(cancellation)?;
1681 let Some(call) = self.shims.process_module_query.as_ref() else {
1682 return Ok(ModuleQueryOutcome::Unsupported);
1683 };
1684 let t = Instant::now();
1685 let result = call.call(
1686 self.environment.clone(),
1687 source.to_owned(),
1688 query.clone(),
1689 options.namespace_context_str().to_owned(),
1690 options.file_label_str().to_owned(),
1691 options.heartbeats(),
1692 options.diagnostic_byte_limit_usize(),
1693 );
1694 self.record_call(0, t.elapsed());
1695 result
1696 }
1697
1698 /// Parse and elaborate a Lean module once, returning several bounded
1699 /// projections keyed by selector id.
1700 ///
1701 /// This is the proof-agent path: Lean owns header handling, one body
1702 /// elaboration, info-tree traversal, and selector projection. Rust sends
1703 /// a small selector array and receives per-selector outcomes; whole-file
1704 /// info-tree arrays never cross the boundary.
1705 ///
1706 /// The shim is optional. When the loaded capability dylib does not
1707 /// export `lean_rs_host_process_module_query_batch`, the method returns
1708 /// [`ModuleQueryBatchOutcome::Unsupported`] without an FFI call.
1709 ///
1710 /// # Errors
1711 ///
1712 /// Returns [`lean_rs::LeanError::Cancelled`] if `cancellation` is
1713 /// already cancelled before dispatch. Returns
1714 /// [`lean_rs::LeanError::LeanException`] if the Lean-side shim raises
1715 /// through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
1716 /// [`HostStage::Conversion`] if the Lean return value does not decode
1717 /// into [`ModuleQueryBatchOutcome`].
1718 pub fn process_module_query_batch(
1719 &mut self,
1720 source: &str,
1721 selectors: &[ModuleQuerySelector],
1722 budgets: &ModuleQueryOutputBudgets,
1723 options: &LeanElabOptions,
1724 cancellation: Option<&LeanCancellationToken>,
1725 ) -> LeanResult<ModuleQueryBatchOutcome> {
1726 let _span = tracing::debug_span!(
1727 target: "lean_rs",
1728 "lean_rs.host.session.process_module_query_batch",
1729 source_len = source.len(),
1730 selectors = selectors.len(),
1731 per_field_bytes = budgets.per_field_bytes,
1732 total_bytes = budgets.total_bytes,
1733 heartbeats = options.heartbeats(),
1734 diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1735 )
1736 .entered();
1737 check_cancellation(cancellation)?;
1738 let Some(call) = self.shims.process_module_query_batch.as_ref() else {
1739 return Ok(ModuleQueryBatchOutcome::Unsupported);
1740 };
1741 let selectors_owned = selectors.to_vec();
1742 let t = Instant::now();
1743 let result = call.call(
1744 self.environment.clone(),
1745 source.to_owned(),
1746 selectors_owned,
1747 budgets.clone(),
1748 options.namespace_context_str().to_owned(),
1749 options.file_label_str().to_owned(),
1750 options.heartbeats(),
1751 options.diagnostic_byte_limit_usize(),
1752 );
1753 self.record_call(u64::try_from(selectors.len()).unwrap_or(u64::MAX), t.elapsed());
1754 result
1755 }
1756
1757 /// Parse/elaborate a Lean module through the shim-owned module snapshot
1758 /// cache, then return bounded selector projections plus cache facts.
1759 ///
1760 /// The snapshot cache remains private to the loaded shim. Rust provides
1761 /// the stable cache key and conservative policy, but never receives raw
1762 /// info trees.
1763 ///
1764 /// # Errors
1765 ///
1766 /// Returns an error if cancellation is already requested, if the shim
1767 /// raises an `IO` exception, or if the Lean result cannot be decoded into
1768 /// the expected cached batch outcome.
1769 pub fn process_module_query_batch_cached(
1770 &mut self,
1771 source: &str,
1772 selectors: &[ModuleQuerySelector],
1773 budgets: &ModuleQueryOutputBudgets,
1774 options: &LeanElabOptions,
1775 policy: &ModuleQueryCachePolicy,
1776 cancellation: Option<&LeanCancellationToken>,
1777 ) -> LeanResult<ModuleQueryBatchCachedOutcome> {
1778 let _span = tracing::debug_span!(
1779 target: "lean_rs",
1780 "lean_rs.host.session.process_module_query_batch_cached",
1781 source_len = source.len(),
1782 selectors = selectors.len(),
1783 per_field_bytes = budgets.per_field_bytes,
1784 total_bytes = budgets.total_bytes,
1785 heartbeats = options.heartbeats(),
1786 diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1787 )
1788 .entered();
1789 check_cancellation(cancellation)?;
1790 let Some(call) = self.shims.process_module_query_batch_cached.as_ref() else {
1791 return Ok(ModuleQueryBatchCachedOutcome::Unsupported);
1792 };
1793 let selectors_owned = selectors.to_vec();
1794 let policy_text = format!(
1795 "{}\n{}\n{}\n{}\n{}",
1796 policy.file_identity, policy.key, policy.max_entries, policy.ttl_millis, policy.max_bytes
1797 );
1798 let t = Instant::now();
1799 let result = call.call(
1800 self.environment.clone(),
1801 source.to_owned(),
1802 selectors_owned,
1803 budgets.clone(),
1804 options.namespace_context_str().to_owned(),
1805 options.file_label_str().to_owned(),
1806 options.heartbeats(),
1807 options.diagnostic_byte_limit_usize(),
1808 policy_text,
1809 );
1810 self.record_call(u64::try_from(selectors.len()).unwrap_or(u64::MAX), t.elapsed());
1811 result
1812 }
1813
1814 /// Try proof snippets against an in-memory source overlay.
1815 ///
1816 /// The shim is optional. When the loaded capability dylib does not export
1817 /// `lean_rs_host_attempt_proof`, the method returns
1818 /// [`ProofAttemptOutcome::Unsupported`] without an FFI call.
1819 ///
1820 /// # Errors
1821 ///
1822 /// Returns an error if cancellation is already requested, if the shim
1823 /// raises an `IO` exception, or if the Lean result cannot be decoded.
1824 pub fn attempt_proof(
1825 &mut self,
1826 request: &ProofAttemptRequest,
1827 options: &LeanElabOptions,
1828 cancellation: Option<&LeanCancellationToken>,
1829 ) -> LeanResult<ProofAttemptOutcome> {
1830 let _span = tracing::debug_span!(
1831 target: "lean_rs",
1832 "lean_rs.host.session.attempt_proof",
1833 source_len = request.source.len(),
1834 candidates = request.candidates.len(),
1835 per_field_bytes = request.budgets.per_field_bytes,
1836 total_bytes = request.budgets.total_bytes,
1837 heartbeats = options.heartbeats(),
1838 diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1839 )
1840 .entered();
1841 check_cancellation(cancellation)?;
1842 let Some(call) = self.shims.attempt_proof.as_ref() else {
1843 return Ok(ProofAttemptOutcome::Unsupported);
1844 };
1845 let t = Instant::now();
1846 let result = call.call(
1847 self.environment.clone(),
1848 request.clone(),
1849 options.namespace_context_str().to_owned(),
1850 options.file_label_str().to_owned(),
1851 options.heartbeats(),
1852 options.diagnostic_byte_limit_usize(),
1853 );
1854 self.record_call(u64::try_from(request.candidates.len()).unwrap_or(u64::MAX), t.elapsed());
1855 result
1856 }
1857
1858 /// Verify one declaration in an in-memory source snapshot.
1859 ///
1860 /// The shim is optional. When the loaded capability dylib does not export
1861 /// `lean_rs_host_verify_declaration`, the method returns
1862 /// [`DeclarationVerificationOutcome::Unsupported`] without an FFI call.
1863 ///
1864 /// # Errors
1865 ///
1866 /// Returns an error if cancellation is already requested, if the shim
1867 /// raises an `IO` exception, or if the Lean result cannot be decoded.
1868 pub fn verify_declaration(
1869 &mut self,
1870 request: &DeclarationVerificationRequest,
1871 options: &LeanElabOptions,
1872 cancellation: Option<&LeanCancellationToken>,
1873 ) -> LeanResult<DeclarationVerificationOutcome> {
1874 let _span = tracing::debug_span!(
1875 target: "lean_rs",
1876 "lean_rs.host.session.verify_declaration",
1877 source_len = request.source.len(),
1878 report_axioms = request.report_axioms,
1879 per_field_bytes = request.budgets.per_field_bytes,
1880 total_bytes = request.budgets.total_bytes,
1881 heartbeats = options.heartbeats(),
1882 diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1883 )
1884 .entered();
1885 check_cancellation(cancellation)?;
1886 let Some(call) = self.shims.verify_declaration.as_ref() else {
1887 return Ok(DeclarationVerificationOutcome::Unsupported);
1888 };
1889 let t = Instant::now();
1890 let result = call.call(
1891 self.environment.clone(),
1892 request.clone(),
1893 options.namespace_context_str().to_owned(),
1894 options.file_label_str().to_owned(),
1895 options.heartbeats(),
1896 options.diagnostic_byte_limit_usize(),
1897 );
1898 self.record_call(1, t.elapsed());
1899 result
1900 }
1901
1902 /// Verify several declarations in one in-memory source snapshot.
1903 ///
1904 /// The shim is optional. When the loaded capability dylib does not export
1905 /// `lean_rs_host_verify_declaration_batch`, the method returns
1906 /// [`DeclarationVerificationBatchOutcome::Unsupported`] without an FFI
1907 /// call. The returned rows preserve request order.
1908 ///
1909 /// # Errors
1910 ///
1911 /// Returns an error if cancellation is already requested, if the shim
1912 /// raises an `IO` exception, or if the Lean result cannot be decoded.
1913 pub fn verify_declaration_batch(
1914 &mut self,
1915 request: &DeclarationVerificationBatchRequest,
1916 options: &LeanElabOptions,
1917 cancellation: Option<&LeanCancellationToken>,
1918 ) -> LeanResult<DeclarationVerificationBatchOutcome> {
1919 let _span = tracing::debug_span!(
1920 target: "lean_rs",
1921 "lean_rs.host.session.verify_declaration_batch",
1922 source_len = request.source.len(),
1923 targets = request.targets.len(),
1924 report_axioms = request.report_axioms,
1925 per_field_bytes = request.budgets.per_field_bytes,
1926 total_bytes = request.budgets.total_bytes,
1927 heartbeats = options.heartbeats(),
1928 diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
1929 )
1930 .entered();
1931 check_cancellation(cancellation)?;
1932 let Some(call) = self.shims.verify_declaration_batch.as_ref() else {
1933 return Ok(DeclarationVerificationBatchOutcome::Unsupported);
1934 };
1935 let t = Instant::now();
1936 let result = call.call(
1937 self.environment.clone(),
1938 request.clone(),
1939 options.namespace_context_str().to_owned(),
1940 options.file_label_str().to_owned(),
1941 options.heartbeats(),
1942 options.diagnostic_byte_limit_usize(),
1943 );
1944 self.record_call(u64::try_from(request.targets.len()).unwrap_or(u64::MAX), t.elapsed());
1945 result
1946 }
1947
1948 /// Clear the shim-owned module snapshot cache when the loaded capability
1949 /// supports it.
1950 ///
1951 /// This releases only cached module snapshots built by the bundled
1952 /// info-tree shim. It does not reset Lean's runtime, unload imported
1953 /// modules, or make full-session compacted regions safe to free.
1954 ///
1955 /// # Errors
1956 ///
1957 /// Returns an error if the shim raises an `IO` exception or if the Lean
1958 /// clear result cannot be decoded.
1959 pub fn clear_module_snapshot_cache(&mut self) -> LeanResult<ModuleSnapshotCacheClearResult> {
1960 let Some(call) = self.shims.clear_module_snapshot_cache.as_ref() else {
1961 return Ok(ModuleSnapshotCacheClearResult {
1962 entries_cleared: 0,
1963 approx_bytes_cleared: 0,
1964 });
1965 };
1966 let t = Instant::now();
1967 let result = call.call();
1968 self.record_call(0, t.elapsed());
1969 result
1970 }
1971
1972 /// Parse and elaborate a single Lean term against the imported
1973 /// environment, optionally against an expected type.
1974 ///
1975 /// The boundary is explicit: Rust supplies the source text, module
1976 /// context, and bounded options; Lean parses, elaborates, and
1977 /// returns either an opaque [`LeanExpr`] handle or a structured
1978 /// [`LeanElabFailure`] carrying typed diagnostics. Rust does not
1979 /// inspect elaborator internals or proof terms to decide
1980 /// correctness.
1981 ///
1982 /// The outer [`LeanResult`] surfaces host-stack failures (a Lean
1983 /// `IO`-level exception from the shim itself, a malformed Lean
1984 /// return value); the inner `Result` distinguishes successful
1985 /// elaboration from parse / type / kernel-stage failures the
1986 /// elaborator reports through its `MessageLog`. Both error paths
1987 /// propagate the [`LeanElabOptions::diagnostic_byte_limit`] bound
1988 /// structurally.
1989 ///
1990 /// # Errors
1991 ///
1992 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side shim raises
1993 /// through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
1994 /// [`HostStage::Conversion`] if the Lean return value does not
1995 /// decode into [`LeanElabFailure`] / [`LeanExpr`].
1996 pub fn elaborate(
1997 &mut self,
1998 source: &str,
1999 expected_type: Option<&LeanExpr<'lean>>,
2000 options: &LeanElabOptions,
2001 cancellation: Option<&LeanCancellationToken>,
2002 ) -> LeanResult<Result<LeanExpr<'lean>, LeanElabFailure>> {
2003 let _span = tracing::debug_span!(
2004 target: "lean_rs",
2005 "lean_rs.host.session.elaborate",
2006 source_len = source.len(),
2007 heartbeats = options.heartbeats(),
2008 diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
2009 )
2010 .entered();
2011 check_cancellation(cancellation)?;
2012 let t = Instant::now();
2013 let result = self.shims.elaborate.call(
2014 self.environment.clone(),
2015 source.to_owned(),
2016 expected_type.cloned(),
2017 options.namespace_context_str().to_owned(),
2018 options.file_label_str().to_owned(),
2019 options.heartbeats(),
2020 options.diagnostic_byte_limit_usize(),
2021 );
2022 self.record_call(0, t.elapsed());
2023 result
2024 }
2025
2026 /// Parse, elaborate, and kernel-check a Lean declaration source
2027 /// (typically a `theorem` or `def`), returning a typed outcome
2028 /// that classifies the result and carries either the produced
2029 /// [`crate::LeanEvidence`] handle or the diagnostics the elaborator and
2030 /// kernel emitted.
2031 ///
2032 /// The boundary is explicit (mirrors [`Self::elaborate`]): Rust
2033 /// supplies source + options; Lean parses, elaborates, runs
2034 /// `addDecl` (which kernel-checks), and classifies the outcome.
2035 /// Rust never inspects the produced proof term or declaration
2036 /// internals.
2037 ///
2038 /// # Errors
2039 ///
2040 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side shim
2041 /// raises through `IO` (an unexpected internal failure that is not
2042 /// itself a rejection / unavailable diagnostic). Returns
2043 /// [`lean_rs::LeanError::Host`] with stage [`HostStage::Conversion`] if the
2044 /// Lean return value does not decode into [`LeanKernelOutcome`].
2045 pub fn kernel_check(
2046 &mut self,
2047 source: &str,
2048 options: &LeanElabOptions,
2049 cancellation: Option<&LeanCancellationToken>,
2050 progress: Option<&dyn LeanProgressSink>,
2051 ) -> LeanResult<LeanKernelOutcome<'lean>> {
2052 let _span = tracing::debug_span!(
2053 target: "lean_rs",
2054 "lean_rs.host.session.kernel_check",
2055 source_len = source.len(),
2056 heartbeats = options.heartbeats(),
2057 diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
2058 )
2059 .entered();
2060 check_cancellation(cancellation)?;
2061 if let Some(sink) = progress {
2062 let bridge = ProgressBridge::new(sink, "kernel_check", Some(1))?;
2063 let (handle, trampoline) = bridge.abi_parts();
2064 let t = Instant::now();
2065 let result = self.shims.kernel_check_progress.call(
2066 self.environment.clone(),
2067 source.to_owned(),
2068 options.namespace_context_str().to_owned(),
2069 options.file_label_str().to_owned(),
2070 options.heartbeats(),
2071 options.diagnostic_byte_limit_usize(),
2072 handle,
2073 trampoline,
2074 );
2075 self.record_call(0, t.elapsed());
2076 bridge.decode(result?)
2077 } else {
2078 let t = Instant::now();
2079 let result = self.shims.kernel_check.call(
2080 self.environment.clone(),
2081 source.to_owned(),
2082 options.namespace_context_str().to_owned(),
2083 options.file_label_str().to_owned(),
2084 options.heartbeats(),
2085 options.diagnostic_byte_limit_usize(),
2086 );
2087 self.record_call(0, t.elapsed());
2088 result
2089 }
2090 }
2091
2092 /// Re-validate a previously captured [`LeanEvidence`] against the
2093 /// session's imported environment, returning the kernel's current
2094 /// verdict.
2095 ///
2096 /// The handle was produced by an earlier
2097 /// [`Self::kernel_check`] call against this same environment and
2098 /// carries the kernel-accepted `Lean.Declaration` opaquely. The
2099 /// session never installs that declaration into its stored
2100 /// environment, so re-checking against the unchanged environment
2101 /// is the supported way to ask "is this evidence still valid?"—
2102 /// the kernel runs fresh.
2103 ///
2104 /// The returned [`EvidenceStatus`] mirrors
2105 /// [`LeanKernelOutcome::status`]: `Checked` on success, `Rejected`
2106 /// if the kernel now refuses the declaration, `Unavailable` if
2107 /// the Lean shim caught an `IO` exception. The Lean fixture does
2108 /// not currently emit `Unsupported` from this path—`Unsupported`
2109 /// only fires during the initial classification in
2110 /// `kernel_check`.
2111 ///
2112 /// # Errors
2113 ///
2114 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean shim raises
2115 /// through `IO` outside of its own `try` (an unexpected internal
2116 /// failure that the shim did not classify). Returns
2117 /// [`lean_rs::LeanError::Host`] with stage [`HostStage::Conversion`] if the
2118 /// return value does not decode as a four-tag
2119 /// [`EvidenceStatus`] inductive.
2120 pub fn check_evidence(
2121 &mut self,
2122 handle: &LeanEvidence<'lean>,
2123 cancellation: Option<&LeanCancellationToken>,
2124 ) -> LeanResult<EvidenceStatus> {
2125 let _span = tracing::debug_span!(
2126 target: "lean_rs",
2127 "lean_rs.host.session.check_evidence",
2128 )
2129 .entered();
2130 check_cancellation(cancellation)?;
2131 let t = Instant::now();
2132 let result = self.shims.check_evidence.call(self.environment.clone(), handle.clone());
2133 self.record_call(0, t.elapsed());
2134 result
2135 }
2136
2137 /// Project a previously captured [`LeanEvidence`] into a bounded
2138 /// [`ProofSummary`] for diagnostics or storage.
2139 ///
2140 /// The Lean shim renders the captured declaration's name, kind,
2141 /// and type expression as three byte-bounded `String`s—no
2142 /// `Lean.Expr` or proof term crosses the FFI boundary. The
2143 /// summary is computed on demand (not at
2144 /// [`Self::kernel_check`] time) because most callers only ever
2145 /// inspect the [`EvidenceStatus`] tag and would pay the
2146 /// pretty-print cost for nothing.
2147 ///
2148 /// Strings on the returned summary are display text. They are not
2149 /// semantic keys; route equality comparisons through a
2150 /// Lean-authored equality export.
2151 ///
2152 /// # Errors
2153 ///
2154 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean shim raises
2155 /// through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
2156 /// [`HostStage::Conversion`] if the return value does not decode
2157 /// as a three-field [`ProofSummary`] structure.
2158 pub fn summarize_evidence(
2159 &mut self,
2160 handle: &LeanEvidence<'lean>,
2161 cancellation: Option<&LeanCancellationToken>,
2162 ) -> LeanResult<ProofSummary> {
2163 let _span = tracing::debug_span!(
2164 target: "lean_rs",
2165 "lean_rs.host.session.summarize_evidence",
2166 )
2167 .entered();
2168 check_cancellation(cancellation)?;
2169 let t = Instant::now();
2170 let result = self
2171 .shims
2172 .evidence_summary
2173 .call(self.environment.clone(), handle.clone());
2174 self.record_call(0, t.elapsed());
2175 result
2176 }
2177
2178 /// Invoke a registered bounded [`MetaM`](https://leanprover.github.io/theorem_proving_in_lean4/)
2179 /// service against the imported environment.
2180 ///
2181 /// The session dispatches through the checked binding for the closed
2182 /// service shape; if the loaded capability does not export the optional
2183 /// symbol, the call short-circuits to [`LeanMetaResponse::Unsupported`]
2184 /// with a synthetic host-side diagnostic naming the missing symbol.
2185 ///
2186 /// The outer [`LeanResult`] surfaces host-stack failures (a Lean
2187 /// `IO`-level exception from the shim itself, or an undecodable
2188 /// return value). The four-way classification—`Ok` / `Failed` /
2189 /// `TimeoutOrHeartbeat` / `Unsupported`—lives in the inner
2190 /// [`LeanMetaResponse`].
2191 ///
2192 /// # Errors
2193 ///
2194 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean shim raises
2195 /// through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
2196 /// [`HostStage::Conversion`] if the return value does not decode
2197 /// into [`LeanMetaResponse<Resp>`].
2198 pub fn run_meta<Req, Resp>(
2199 &mut self,
2200 service: &LeanMetaService<Req, Resp>,
2201 request: Req,
2202 options: &LeanMetaOptions,
2203 cancellation: Option<&LeanCancellationToken>,
2204 ) -> LeanResult<LeanMetaResponse<Resp>>
2205 where
2206 LeanMetaService<Req, Resp>: HostMetaDispatch<'lean, Req, Resp>,
2207 Req: lean_rs::abi::traits::LeanAbi<'lean>,
2208 Resp: TryFromLean<'lean>,
2209 {
2210 let _span = tracing::debug_span!(
2211 target: "lean_rs",
2212 "lean_rs.host.session.run_meta",
2213 service = service.name(),
2214 heartbeats = options.heartbeats(),
2215 diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
2216 )
2217 .entered();
2218 check_cancellation(cancellation)?;
2219 service.dispatch(self, request, options)
2220 }
2221
2222 /// Look up many declarations in one Lean traversal.
2223 ///
2224 /// Equivalent to calling [`Self::query_declaration`] in a loop over
2225 /// `names`, except that the entire batch crosses the FFI boundary
2226 /// exactly once: one `Array Name` allocation in, one
2227 /// `Array (Option Declaration)` allocation out. The Lean shim folds
2228 /// the singular `envQueryDeclaration` across the input array, so the
2229 /// iteration semantics are identical to a Rust-side fold over the
2230 /// singular path—a missing name still errors the batch.
2231 ///
2232 /// Names are still resolved through the capability's
2233 /// `name_from_string` shim, one [`lean_rs::LeanName`] handle per
2234 /// input. The metric impact is `names.len() + 1` recorded FFI calls
2235 /// for a batch of `names.len()` items, versus `2 * names.len()` for
2236 /// the same workload through [`Self::query_declaration`].
2237 ///
2238 /// # Errors
2239 ///
2240 /// Returns [`lean_rs::LeanError::Host`] with stage [`HostStage::Conversion`]
2241 /// on the first name that is not present in the imported
2242 /// environment, with the missing name in the diagnostic. Returns
2243 /// [`lean_rs::LeanError::LeanException`] if the Lean-side bulk shim raises
2244 /// through `IO`.
2245 pub fn query_declarations_bulk(
2246 &mut self,
2247 names: &[&str],
2248 cancellation: Option<&LeanCancellationToken>,
2249 progress: Option<&dyn LeanProgressSink>,
2250 ) -> LeanResult<Vec<LeanDeclaration<'lean>>> {
2251 let _span = tracing::debug_span!(
2252 target: "lean_rs",
2253 "lean_rs.host.session.query_declarations_bulk",
2254 batch_size = names.len(),
2255 )
2256 .entered();
2257 if names.is_empty() {
2258 return Ok(Vec::new());
2259 }
2260 check_cancellation(cancellation)?;
2261 if cancellation.is_some() {
2262 let started = Instant::now();
2263 let mut out = Vec::with_capacity(names.len());
2264 let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
2265 for (idx, name) in names.iter().enumerate() {
2266 check_cancellation(cancellation)?;
2267 out.push(self.query_declaration(name, cancellation)?);
2268 report_progress(
2269 progress,
2270 "query_declarations_bulk",
2271 u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
2272 total,
2273 started,
2274 )?;
2275 }
2276 return Ok(out);
2277 }
2278 let prepare_started = Instant::now();
2279 let total = Some(u64::try_from(names.len()).unwrap_or(u64::MAX));
2280 let mut name_handles: Vec<LeanName<'lean>> = Vec::with_capacity(names.len());
2281 for (idx, name) in names.iter().enumerate() {
2282 name_handles.push(self.make_name(name, cancellation)?);
2283 report_progress(
2284 progress,
2285 "prepare_names",
2286 u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
2287 total,
2288 prepare_started,
2289 )?;
2290 }
2291 check_cancellation(cancellation)?;
2292 let raw = if let Some(sink) = progress {
2293 let bridge = ProgressBridge::new(sink, "query_declarations_bulk", total)?;
2294 let (handle, trampoline) = bridge.abi_parts();
2295 let t = Instant::now();
2296 let result = self.shims.env_query_declarations_bulk_progress.call(
2297 self.environment.clone(),
2298 name_handles,
2299 handle,
2300 trampoline,
2301 );
2302 let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
2303 self.record_call(batch_len, t.elapsed());
2304 bridge.decode::<Vec<Option<LeanDeclaration<'lean>>>>(result?)?
2305 } else {
2306 let t = Instant::now();
2307 let result = self
2308 .shims
2309 .env_query_declarations_bulk
2310 .call(self.environment.clone(), name_handles);
2311 let batch_len = u64::try_from(names.len()).unwrap_or(u64::MAX);
2312 self.record_call(batch_len, t.elapsed());
2313 result?
2314 };
2315 let mut out: Vec<LeanDeclaration<'lean>> = Vec::with_capacity(raw.len());
2316 for (slot, name) in raw.into_iter().zip(names.iter()) {
2317 match slot {
2318 Some(decl) => out.push(decl),
2319 None => {
2320 return Err(lean_rs::abi::traits::conversion_error(format!(
2321 "declaration '{name}' not found in imported environment"
2322 )));
2323 }
2324 }
2325 }
2326 Ok(out)
2327 }
2328
2329 /// Parse and elaborate many independent Lean terms in one Lean
2330 /// traversal.
2331 ///
2332 /// Per-source `Result<LeanExpr, LeanElabFailure>` shape matches
2333 /// [`Self::elaborate`] exactly: outer [`LeanResult`] surfaces
2334 /// host-stack failures, inner per-source `Result` distinguishes
2335 /// successful elaboration from elaborator-reported diagnostics. A
2336 /// caller treating the bulk path as a fold over the singular path
2337 /// sees no semantic surprise.
2338 ///
2339 /// The `expected_type` parameter is **not** carried by the bulk
2340 /// shape: per-source expectations would force a parallel
2341 /// `&[Option<&LeanExpr>]` array, and no in-tree caller has earned
2342 /// the surface. Use [`Self::elaborate`] for individual terms with
2343 /// expected types.
2344 ///
2345 /// The heartbeat and diagnostic-byte budgets in `options` apply
2346 /// once each per source (the Lean shim builds fresh
2347 /// [`Lean.Options`](https://leanprover.github.io/) per item via the
2348 /// same `hostElaborate` path), so the per-batch upper bound on
2349 /// elapsed CPU work is `sources.len() * options.heartbeats()`.
2350 ///
2351 /// # Errors
2352 ///
2353 /// Returns [`lean_rs::LeanError::LeanException`] if the Lean-side bulk shim
2354 /// raises through `IO`. Returns [`lean_rs::LeanError::Host`] with stage
2355 /// [`HostStage::Conversion`] if the Lean return value does not
2356 /// decode into a `Vec<Result<LeanExpr, LeanElabFailure>>`.
2357 pub fn elaborate_bulk(
2358 &mut self,
2359 sources: &[&str],
2360 options: &LeanElabOptions,
2361 cancellation: Option<&LeanCancellationToken>,
2362 progress: Option<&dyn LeanProgressSink>,
2363 ) -> LeanResult<Vec<Result<LeanExpr<'lean>, LeanElabFailure>>> {
2364 let _span = tracing::debug_span!(
2365 target: "lean_rs",
2366 "lean_rs.host.session.elaborate_bulk",
2367 batch_size = sources.len(),
2368 heartbeats = options.heartbeats(),
2369 diagnostic_byte_limit = options.diagnostic_byte_limit_usize(),
2370 )
2371 .entered();
2372 if sources.is_empty() {
2373 return Ok(Vec::new());
2374 }
2375 check_cancellation(cancellation)?;
2376 if cancellation.is_some() {
2377 let started = Instant::now();
2378 let total = Some(u64::try_from(sources.len()).unwrap_or(u64::MAX));
2379 let mut out = Vec::with_capacity(sources.len());
2380 for (idx, source) in sources.iter().enumerate() {
2381 check_cancellation(cancellation)?;
2382 out.push(self.elaborate(source, None, options, cancellation)?);
2383 report_progress(
2384 progress,
2385 "elaborate_bulk",
2386 u64::try_from(idx.saturating_add(1)).unwrap_or(u64::MAX),
2387 total,
2388 started,
2389 )?;
2390 }
2391 return Ok(out);
2392 }
2393 let sources_owned: Vec<String> = sources.iter().map(|&s| s.to_owned()).collect();
2394 if let Some(sink) = progress {
2395 let total = Some(u64::try_from(sources.len()).unwrap_or(u64::MAX));
2396 let bridge = ProgressBridge::new(sink, "elaborate_bulk", total)?;
2397 let (handle, trampoline) = bridge.abi_parts();
2398 let t = Instant::now();
2399 let result = self.shims.elaborate_bulk_progress.call(
2400 self.environment.clone(),
2401 sources_owned,
2402 options.namespace_context_str().to_owned(),
2403 options.file_label_str().to_owned(),
2404 options.heartbeats(),
2405 options.diagnostic_byte_limit_usize(),
2406 handle,
2407 trampoline,
2408 );
2409 let batch_len = u64::try_from(sources.len()).unwrap_or(u64::MAX);
2410 self.record_call(batch_len, t.elapsed());
2411 bridge.decode(result?)
2412 } else {
2413 let t = Instant::now();
2414 let result = self.shims.elaborate_bulk.call(
2415 self.environment.clone(),
2416 sources_owned,
2417 options.namespace_context_str().to_owned(),
2418 options.file_label_str().to_owned(),
2419 options.heartbeats(),
2420 options.diagnostic_byte_limit_usize(),
2421 );
2422 let batch_len = u64::try_from(sources.len()).unwrap_or(u64::MAX);
2423 self.record_call(batch_len, t.elapsed());
2424 result
2425 }
2426 }
2427
2428 /// Build a `LeanName` from a dotted Rust string via the capability's
2429 /// `Name.toName` shim.
2430 fn make_name(&self, name: &str, cancellation: Option<&LeanCancellationToken>) -> LeanResult<LeanName<'lean>> {
2431 check_cancellation(cancellation)?;
2432 let lean_name = lean_rs::__host_internals::string_from_str(self.capabilities.host().runtime(), name);
2433 let t = Instant::now();
2434 let result = self.shims.name_from_string.call(lean_name);
2435 self.record_call(0, t.elapsed());
2436 result
2437 }
2438}
2439
2440trait HostMetaDispatch<'lean, Req, Resp> {
2441 fn dispatch(
2442 &self,
2443 session: &mut LeanSession<'lean, '_>,
2444 request: Req,
2445 options: &LeanMetaOptions,
2446 ) -> LeanResult<LeanMetaResponse<Resp>>;
2447}
2448
2449impl<'lean> HostMetaDispatch<'lean, LeanExpr<'lean>, LeanExpr<'lean>>
2450 for LeanMetaService<LeanExpr<'lean>, LeanExpr<'lean>>
2451{
2452 fn dispatch(
2453 &self,
2454 session: &mut LeanSession<'lean, '_>,
2455 request: LeanExpr<'lean>,
2456 options: &LeanMetaOptions,
2457 ) -> LeanResult<LeanMetaResponse<LeanExpr<'lean>>> {
2458 let Some(call) = (match self.name() {
2459 "lean_rs_host_meta_infer_type" => session.shims.meta_infer_type.as_ref(),
2460 "lean_rs_host_meta_whnf" => session.shims.meta_whnf.as_ref(),
2461 "lean_rs_host_meta_heartbeat_burn" => session.shims.meta_heartbeat_burn.as_ref(),
2462 _ => None,
2463 }) else {
2464 return Ok(unsupported_meta_response(self.name()));
2465 };
2466 let t = Instant::now();
2467 let result = call.call(
2468 session.environment.clone(),
2469 request,
2470 options.heartbeats(),
2471 options.diagnostic_byte_limit_usize(),
2472 options.transparency_byte(),
2473 );
2474 session.record_call(0, t.elapsed());
2475 result
2476 }
2477}
2478
2479impl<'lean>
2480 HostMetaDispatch<
2481 'lean,
2482 (
2483 LeanExpr<'lean>,
2484 LeanExpr<'lean>,
2485 crate::host::meta::LeanMetaTransparency,
2486 ),
2487 bool,
2488 >
2489 for LeanMetaService<
2490 (
2491 LeanExpr<'lean>,
2492 LeanExpr<'lean>,
2493 crate::host::meta::LeanMetaTransparency,
2494 ),
2495 bool,
2496 >
2497{
2498 fn dispatch(
2499 &self,
2500 session: &mut LeanSession<'lean, '_>,
2501 request: (
2502 LeanExpr<'lean>,
2503 LeanExpr<'lean>,
2504 crate::host::meta::LeanMetaTransparency,
2505 ),
2506 options: &LeanMetaOptions,
2507 ) -> LeanResult<LeanMetaResponse<bool>> {
2508 let Some(call) = session.shims.meta_is_def_eq.as_ref() else {
2509 return Ok(unsupported_meta_response(self.name()));
2510 };
2511 let t = Instant::now();
2512 let result = call.call(
2513 session.environment.clone(),
2514 request,
2515 options.heartbeats(),
2516 options.diagnostic_byte_limit_usize(),
2517 options.transparency_byte(),
2518 );
2519 session.record_call(0, t.elapsed());
2520 result
2521 }
2522}
2523
2524impl<'lean> HostMetaDispatch<'lean, LeanExpr<'lean>, String> for LeanMetaService<LeanExpr<'lean>, String> {
2525 fn dispatch(
2526 &self,
2527 session: &mut LeanSession<'lean, '_>,
2528 request: LeanExpr<'lean>,
2529 options: &LeanMetaOptions,
2530 ) -> LeanResult<LeanMetaResponse<String>> {
2531 let Some(call) = session.shims.meta_pp_expr.as_ref() else {
2532 return Ok(unsupported_meta_response(self.name()));
2533 };
2534 let t = Instant::now();
2535 let result = call.call(
2536 session.environment.clone(),
2537 request,
2538 options.heartbeats(),
2539 options.diagnostic_byte_limit_usize(),
2540 options.transparency_byte(),
2541 );
2542 session.record_call(0, t.elapsed());
2543 result
2544 }
2545}
2546
2547fn unsupported_meta_response<Resp>(symbol: &str) -> LeanMetaResponse<Resp> {
2548 LeanMetaResponse::Unsupported(LeanElabFailure::synthetic(
2549 format!("bundled host shim does not export meta service '{symbol}'"),
2550 "<lean-rs-host meta>".to_owned(),
2551 ))
2552}