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