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