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