lean-rs-host 0.1.1

Opinionated Rust host stack for embedding Lean 4 as a theorem-prover capability: typed sessions, kernel-check evidence handles, bounded MetaM services, progress, batching, and session pooling.
# lean-rs-host

Opinionated Rust host stack for embedding Lean 4 as a theorem-prover capability. Provides the
`LeanHost` / `LeanCapabilities` / `LeanSession` trio, the kernel-check evidence types
(`LeanEvidence`, `LeanKernelOutcome`, `ProofSummary`), the typed elaboration diagnostics
(`LeanElabOptions`, `LeanElabFailure`, `LeanDiagnostic`, `LeanSeverity`, `LeanPosition`), the
bounded `MetaM` service surface at `lean_rs_host::meta::*`, and the `SessionPool` /
`PooledSession` reuse helper.

Built on top of [`lean-rs`](https://docs.rs/lean-rs), the typed-FFI primitive. The opaque
semantic handles `LeanName`, `LeanLevel`, `LeanExpr`, and `LeanDeclaration` live on `lean-rs`;
this crate consumes them through `use lean_rs::{...}`. If you only need to call typed
`@[export]` Lean functions from Rust, depend on `lean-rs` directly: it is the typed-FFI
minimum and has no Lean-side shim contract.

**You write zero shim exports yourself.** `lean-rs-host` bundles its host and generic
interop shim packages and builds them on demand. Your `lakefile.lean` declares only your
own capability library; you do not `require lean_rs_host_shims`, write
`lean_rs_host_*` exports, or import the shim package from Lean.

The three central types nest:

```text
LeanHost            opened once per Lean runtime
  └─ LeanCapabilities   loaded once per (consumer dylib + shim dylib) pair
       └─ LeanSession   one per call, returned from caps.session(...)
```

Hold a `LeanHost` for the process lifetime, share a `LeanCapabilities` across calls into
the same capability, and open a fresh `LeanSession` for each unit of work.

Supports the same Lean toolchain window as
[`lean-rs-sys`](https://docs.rs/lean-rs-sys): currently **Lean 4.26.0 through 4.29.1**; see
[`docs/version-matrix.md`](https://github.com/jcreinhold/lean-rs/blob/main/docs/version-matrix.md).
The capability loader transparently handles the Lake naming-convention change between Lean
4.26 and 4.27 (dylib filename and module-initializer symbol shape), so consumer
`lakefile.lean`s do not need version-conditional logic.

## Quick start

Add a `lean-rs-host` dependency to `Cargo.toml`. The crate ships the matching host and
generic interop shim sources and builds them on demand, so your `lakefile.lean` declares
only your own capability library. Everything else mirrors the same-process setup in
[`lean-rs`'s README](https://docs.rs/lean-rs).

**`Cargo.toml`**:

```toml
[package]
name = "my_app"
version = "0.1.0"
edition = "2024"

[dependencies]
lean-rs = "0.1"
lean-rs-host = "0.1"

[build-dependencies]
lean-toolchain = "0.1"
```

**`build.rs`**: build your capability with the same shipped-crate helper used by
`lean-rs` applications. `lean-rs-host` builds its bundled host shims on demand, but your
consumer capability still needs a Lake shared-library target:

```rust
fn main() -> Result<(), Box<dyn std::error::Error>> {
    lean_toolchain::CargoLeanCapability::new("lean", "MyCapability")
        .package("my_app")
        .module("MyCapability")
        .build()?;
    Ok(())
}
```

**`lean/lakefile.lean`**: declares your capability target. Do not add a
`lean_rs_host_shims` require; `lean-rs-host` loads its bundled shims separately.
Lake uses guillemets (`« »`) as its idiomatic quoting for package and library names;
plain ASCII names also work.

Long-running imports, bulk introspection, filtered listing, and kernel-check
calls can receive a borrowed `LeanProgressSink` for live in-thread progress
events. Passing `None` keeps the no-progress fast path.

```lean
import Lake
open Lake DSL

package «my_app»

@[default_target]
lean_lib «MyCapability» where
  defaultFacets := #[LeanLib.sharedFacet]
```

**`lean/MyCapability.lean`**: one Rust-callable export.

```lean
@[export my_app_square]
def square (n : UInt64) : UInt64 := n * n
```

**`src/main.rs`**—open the Lake project as a `LeanHost`, load capabilities, drive a session:

```rust
use std::path::PathBuf;
use lean_rs::LeanResult;
use lean_rs::LeanRuntime;
use lean_rs_host::{LeanElabOptions, LeanHost};

fn main() -> LeanResult<()> {
    let lake_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("lean");

    let runtime = LeanRuntime::init()?;
    let host = LeanHost::from_lake_project(runtime, &lake_root)?;
    let caps = host.load_capabilities("my_app", "MyCapability")?;
    let mut session = caps.session(&["MyCapability"], None, None)?;

    // Elaborate a Lean term in the session's environment.
    let opts = LeanElabOptions::new();
    let elaborated = session.elaborate("(1 + 2 : Nat)", None, &opts, None)?;
    println!("elaborate ok: {}", elaborated.is_ok());

    // Call your own typed @[export] through the instrumented session dispatch.
    let n = session.call_capability::<(u64,), u64>("my_app_square", (7u64,), None)?;
    println!("square(7) = {n}");  // 49

    Ok(())
}
```

Build and run:

```sh
cargo run
```

`CargoLeanCapability` runs `lake build MyCapability:shared`, emits Cargo rerun and link
directives, and exposes the built dylib path at compile time. `load_capabilities` also builds
and opens the crate-bundled `LeanRsInterop` and `LeanRsHostShims` dylibs, sharing one Lean
runtime; per-module `initialize_*` functions are idempotent.

## Capability contract

The full per-symbol contract (each Lean signature, the Rust call site it maps to, and the
typed `LeanSession::*` method on top) lives at
[`docs/lean-rs-host-capability-contract.md`](https://github.com/jcreinhold/lean-rs/blob/main/docs/lean-rs-host-capability-contract.md).

## Caveats

**Nullary unboxed-scalar globals trip the function-path dispatch.** A nullary `@[export]`
returning an unboxed scalar (e.g., `def decideTrue : Bool := decide (1 + 1 = 2)`) is
compiled by Lake as a persistent global, not a function symbol. `LeanModule::exported`'s
function-path dispatch then reads the global's stored scalar-tagged value as if it were a
function pointer, and `.call(...)` panics with `misaligned pointer dereference`. Workaround:
add a `Unit` argument so Lake emits a function symbol:

```lean
def decideTrue (_ : Unit) : Bool := decide (1 + 1 = 2)
```

The Rust call site then becomes `module.exported::<((),), bool>(...).call(())`.

## Worked examples

Eight runnable examples under
[`crates/lean-rs-host/examples/`](https://github.com/jcreinhold/lean-rs/tree/main/crates/lean-rs-host/examples)
drive `lean_rs_host::*` end to end against the in-tree fixture:

- `theorem_query`—open a session, contrast a definition's `kind` with a theorem's.
- `evaluate`—call a typed `@[export]` through `LeanSession::call_capability`.
- `proof_check`—kernel-check a theorem, re-validate the evidence, render the summary.
- `meta_query`—run a bounded `MetaM` service and branch on every status.
- `progress`—attach a `LeanProgressSink` and trigger cooperative cancellation.
- `tour`—the four flows composed end to end in one process.
- `lake_build_helper`—build a Lake shared-library target through `lean-toolchain`.
- `long_session_memory`—capture RSS checkpoints for a long-lived session workload.

See the
[examples README](https://github.com/jcreinhold/lean-rs/blob/main/crates/lean-rs-host/examples/README.md)
for the per-example walkthrough, expected output, and common failures.

## License

Dual-licensed under MIT or Apache-2.0 at your option.