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