#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::panic)]
use std::path::PathBuf;
use std::time::Instant;
use lean_rs::module::LeanIo;
use lean_rs::{HostStage, LeanError, LeanRuntime};
use lean_rs_host::{LeanCapabilities, LeanElabOptions, LeanHost, LeanSession, SessionPool};
fn fixture_lake_root() -> PathBuf {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let workspace = manifest_dir
.parent()
.and_then(std::path::Path::parent)
.expect("crates/<name>/ lives two directories beneath the workspace root");
workspace.join("fixtures").join("lean")
}
fn runtime() -> &'static LeanRuntime {
LeanRuntime::init().expect("Lean runtime initialisation must succeed")
}
fn fixture_host() -> LeanHost<'static> {
LeanHost::from_lake_project(runtime(), fixture_lake_root()).expect("host opens cleanly")
}
fn session_over_handles<'lean, 'c>(caps: &'c LeanCapabilities<'lean, 'c>) -> LeanSession<'lean, 'c> {
caps.session(&["LeanRsFixture.Handles"])
.expect("session imports cleanly")
}
fn session_over_elaboration<'lean, 'c>(caps: &'c LeanCapabilities<'lean, 'c>) -> LeanSession<'lean, 'c> {
caps.session(&["LeanRsHostShims.Elaboration"])
.expect("session imports cleanly")
}
#[test]
fn query_declarations_bulk_returns_all_for_existing_names() {
let host = fixture_host();
let caps = host
.load_capabilities("lean_rs_fixture", "LeanRsFixture")
.expect("load caps");
let mut session = session_over_handles(&caps);
let baseline = session.stats();
let names = [
"LeanRsFixture.Handles.nameAnonymous",
"LeanRsFixture.Handles.nameMkStr",
"LeanRsFixture.Handles.exprConstNat",
];
let decls = session
.query_declarations_bulk(&names)
.expect("bulk query succeeds for fully-resolvable name list");
assert_eq!(decls.len(), 3, "bulk returns one slot per input name");
drop(decls);
let after = session.stats();
assert_eq!(
after.ffi_calls - baseline.ffi_calls,
4,
"bulk path costs N + 1 FFI calls (one bulk + N name_from_string), got delta {}",
after.ffi_calls - baseline.ffi_calls,
);
assert_eq!(
after.batch_items - baseline.batch_items,
3,
"batch_items records the per-item batch length",
);
}
#[test]
fn query_declarations_bulk_errors_on_missing_name() {
let host = fixture_host();
let caps = host
.load_capabilities("lean_rs_fixture", "LeanRsFixture")
.expect("load caps");
let mut session = session_over_handles(&caps);
let err = session
.query_declarations_bulk(&[
"LeanRsFixture.Handles.nameAnonymous",
"This.Name.Does.Not.Exist",
"LeanRsFixture.Handles.nameMkStr",
])
.expect_err("bulk must error when any input name is missing");
match err {
LeanError::Host(failure) => {
assert_eq!(failure.stage(), HostStage::Conversion);
assert!(
failure.message().contains("This.Name.Does.Not.Exist"),
"diagnostic must name the missing entry, got: {:?}",
failure.message(),
);
}
LeanError::LeanException(exc) => panic!("expected Host(Conversion), got LeanException {exc:?}"),
_ => panic!("LeanError gained a new variant; update this match"),
}
}
#[test]
fn query_declarations_bulk_empty_input_is_no_op() {
let host = fixture_host();
let caps = host
.load_capabilities("lean_rs_fixture", "LeanRsFixture")
.expect("load caps");
let mut session = session_over_handles(&caps);
let baseline = session.stats();
let decls = session
.query_declarations_bulk(&[])
.expect("empty input returns empty vec");
assert!(decls.is_empty(), "empty input yields empty output");
assert_eq!(session.stats(), baseline, "empty bulk must not record an FFI call");
}
#[test]
fn elaborate_bulk_returns_per_source_results() {
let host = fixture_host();
let caps = host
.load_capabilities("lean_rs_fixture", "LeanRsFixture")
.expect("load caps");
let mut session = session_over_elaboration(&caps);
let baseline = session.stats();
let opts = LeanElabOptions::new();
let outcomes = session
.elaborate_bulk(&["(1 + 2 : Nat)", "1 +", "(1 + \"hi\" : Nat)"], &opts)
.expect("bulk elaborate routes through the host stack cleanly");
assert_eq!(outcomes.len(), 3, "bulk returns one slot per input source");
assert!(outcomes[0].is_ok(), "first source elaborates successfully");
assert!(outcomes[1].is_err(), "second source is a parse failure");
assert!(outcomes[2].is_err(), "third source is a type-mismatch failure");
let after = session.stats();
assert_eq!(
after.ffi_calls - baseline.ffi_calls,
1,
"elaborate_bulk dispatches once regardless of batch size",
);
assert_eq!(
after.batch_items - baseline.batch_items,
3,
"batch_items records the per-source batch length",
);
}
#[test]
fn elaborate_bulk_empty_input_is_no_op() {
let host = fixture_host();
let caps = host
.load_capabilities("lean_rs_fixture", "LeanRsFixture")
.expect("load caps");
let mut session = session_over_elaboration(&caps);
let baseline = session.stats();
let outcomes = session
.elaborate_bulk(&[], &LeanElabOptions::new())
.expect("empty input returns empty vec");
assert!(outcomes.is_empty(), "empty input yields empty output");
assert_eq!(session.stats(), baseline, "empty bulk must not record an FFI call");
}
#[test]
fn call_capability_dispatches_pure_fixture_export() {
let host = fixture_host();
let caps = host
.load_capabilities("lean_rs_fixture", "LeanRsFixture")
.expect("load caps");
let mut session = session_over_handles(&caps);
let baseline = session.stats();
let product: u64 = session
.call_capability::<(u64, u64), u64>("lean_rs_fixture_u64_mul", (3, 4))
.expect("call_capability dispatches the pure export");
assert_eq!(product, 12);
let after = session.stats();
assert_eq!(
after.ffi_calls - baseline.ffi_calls,
1,
"call_capability records exactly one FFI dispatch",
);
assert_eq!(
after.batch_items - baseline.batch_items,
0,
"call_capability is not a bulk operation",
);
}
#[test]
fn call_capability_dispatches_io_fixture_export() {
let host = fixture_host();
let caps = host
.load_capabilities("lean_rs_fixture", "LeanRsFixture")
.expect("load caps");
let mut session = session_over_handles(&caps);
session
.call_capability::<(), LeanIo<()>>("lean_rs_fixture_io_success_unit", ())
.expect("call_capability dispatches the IO export");
}
#[test]
fn call_capability_unknown_symbol_is_link_error() {
let host = fixture_host();
let caps = host
.load_capabilities("lean_rs_fixture", "LeanRsFixture")
.expect("load caps");
let mut session = session_over_handles(&caps);
let err = session
.call_capability::<(), LeanIo<u64>>("lean_rs_fixture_no_such_export", ())
.expect_err("missing symbol must surface as a host link error");
match err {
LeanError::Host(failure) => {
assert_eq!(failure.stage(), HostStage::Link);
assert!(
failure.message().contains("lean_rs_fixture_no_such_export"),
"diagnostic must name the missing symbol, got: {:?}",
failure.message(),
);
}
LeanError::LeanException(exc) => panic!("expected Host(Link) failure, got LeanException {exc:?}"),
_ => panic!("LeanError gained a new variant; update this match"),
}
}
#[test]
fn session_pool_reuses_session() {
let runtime = runtime();
let host = LeanHost::from_lake_project(runtime, fixture_lake_root()).expect("host opens");
let caps = host
.load_capabilities("lean_rs_fixture", "LeanRsFixture")
.expect("load caps");
let pool = SessionPool::with_capacity(runtime, 4);
let imports = ["LeanRsFixture.Handles"];
{
let mut sess = pool.acquire(&caps, &imports).expect("first acquire imports fresh");
let kind = sess
.declaration_kind("LeanRsFixture.Handles.nameAnonymous")
.expect("query");
assert_eq!(kind, "definition");
}
{
let mut sess = pool
.acquire(&caps, &imports)
.expect("second acquire reuses the released env");
let kind = sess
.declaration_kind("LeanRsFixture.Handles.nameAnonymous")
.expect("query");
assert_eq!(kind, "definition");
}
let stats = pool.stats();
assert_eq!(stats.imports_performed, 1, "second acquire must reuse, not re-import");
assert_eq!(stats.reused, 1, "one acquire was a cache hit");
assert_eq!(stats.acquired, 2, "both acquires accounted for");
assert_eq!(stats.released_to_pool, 2, "both Drops returned to the pool");
assert_eq!(stats.released_dropped, 0, "capacity 4 is well above 1 live session");
}
#[test]
fn session_pool_capacity_caps_storage() {
let runtime = runtime();
let host = LeanHost::from_lake_project(runtime, fixture_lake_root()).expect("host opens");
let caps = host
.load_capabilities("lean_rs_fixture", "LeanRsFixture")
.expect("load caps");
let pool = SessionPool::with_capacity(runtime, 1);
let imports = ["LeanRsFixture.Handles"];
let s1 = pool.acquire(&caps, &imports).expect("acquire #1");
let s2 = pool.acquire(&caps, &imports).expect("acquire #2");
drop(s1);
drop(s2);
assert_eq!(pool.len(), 1, "free list must not exceed capacity");
let stats = pool.stats();
assert_eq!(
stats.imports_performed, 2,
"no entries on free list during acquire phase"
);
assert_eq!(stats.released_to_pool, 1, "first release fits under capacity");
assert_eq!(stats.released_dropped, 1, "second release drops the env (pool full)");
}
#[test]
fn session_pool_distinct_imports_do_not_match() {
let runtime = runtime();
let host = LeanHost::from_lake_project(runtime, fixture_lake_root()).expect("host opens");
let caps = host
.load_capabilities("lean_rs_fixture", "LeanRsFixture")
.expect("load caps");
let pool = SessionPool::with_capacity(runtime, 4);
drop(pool.acquire(&caps, &["LeanRsFixture.Handles"]).expect("acquire A"));
drop(
pool.acquire(&caps, &["LeanRsHostShims.Elaboration"])
.expect("acquire B"),
);
let stats = pool.stats();
assert_eq!(
stats.imports_performed, 2,
"different import lists are different cache keys; both must import fresh",
);
assert_eq!(stats.reused, 0, "no key collision possible across distinct imports");
assert_eq!(pool.len(), 2, "both envs sit on the free list");
}
#[test]
fn session_pool_zero_capacity_never_reuses() {
let runtime = runtime();
let host = LeanHost::from_lake_project(runtime, fixture_lake_root()).expect("host opens");
let caps = host
.load_capabilities("lean_rs_fixture", "LeanRsFixture")
.expect("load caps");
let pool = SessionPool::with_capacity(runtime, 0);
let imports = ["LeanRsFixture.Handles"];
drop(pool.acquire(&caps, &imports).expect("acquire #1"));
drop(pool.acquire(&caps, &imports).expect("acquire #2"));
let stats = pool.stats();
assert_eq!(stats.imports_performed, 2, "capacity 0 degenerates to always-import");
assert_eq!(stats.released_dropped, 2, "every release drops");
assert_eq!(pool.len(), 0, "free list never holds anything");
}
#[test]
fn bulk_vs_singular_timing_note() {
const ITEMS: usize = 8;
let host = fixture_host();
let caps = host
.load_capabilities("lean_rs_fixture", "LeanRsFixture")
.expect("load caps");
let mut session = session_over_handles(&caps);
let names: [&str; ITEMS] = [
"LeanRsFixture.Handles.nameAnonymous",
"LeanRsFixture.Handles.nameMkStr",
"LeanRsFixture.Handles.nameMkNum",
"LeanRsFixture.Handles.nameToString",
"LeanRsFixture.Handles.nameBeq",
"LeanRsFixture.Handles.levelZero",
"LeanRsFixture.Handles.levelSucc",
"LeanRsFixture.Handles.exprConstNat",
];
let start_singular = Instant::now();
for name in names {
session.query_declaration(name).expect("singular query for known name");
}
let singular_elapsed = start_singular.elapsed();
let start_bulk = Instant::now();
let decls = session
.query_declarations_bulk(&names)
.expect("bulk query for known names");
let bulk_elapsed = start_bulk.elapsed();
assert_eq!(decls.len(), ITEMS);
println!(
"bulk_vs_singular_timing_note: \
{ITEMS} singular queries took {singular_elapsed:?}; \
one bulk query took {bulk_elapsed:?}",
);
}