use oxilean_kernel::{
check_constant_info, fuel, init_builtin_env, AxiomVal, BinderInfo, ConstantInfo, ConstantVal,
DefinitionSafety, DefinitionVal, Environment, Expr, KernelError, Level, Literal, Name, Node,
Reducer, ReducibilityHint,
};
fn env() -> Environment {
let mut env = Environment::new();
match init_builtin_env(&mut env) {
Ok(()) => env,
Err(e) => unreachable!("builtin env must initialize: {e}"),
}
}
fn nat(n: u64) -> Expr {
Expr::Lit(Literal::nat(n))
}
fn cnst(name: &str) -> Expr {
Expr::Const(Name::str(name), vec![])
}
fn app(f: Expr, a: Expr) -> Expr {
Expr::App(Node::new(f), Node::new(a))
}
fn sum(k: u64) -> Expr {
let mut term = nat(0);
for _ in 0..k {
term = app(app(cnst("Nat.add"), term), nat(1));
}
term
}
fn on_a_deep_stack<T: Send + 'static>(work: impl FnOnce() -> T + Send + 'static) -> T {
std::thread::Builder::new()
.stack_size(256 << 20)
.spawn(work)
.unwrap_or_else(|e| panic!("cannot spawn the test thread: {e}"))
.join()
.unwrap_or_else(|_| panic!("the test thread panicked"))
}
#[test]
fn a_reduction_nested_within_the_limit_runs_to_its_value() {
on_a_deep_stack(|| {
let env = env();
fuel::set_budget(None);
fuel::set_reduction_nesting_limit(Some(64));
let value = Reducer::new().whnf_env(&sum(40), &env);
assert_eq!(value, nat(40));
assert!(!fuel::reduction_limit_hit());
fuel::set_reduction_nesting_limit(None);
});
}
#[test]
fn a_reduction_nested_deeper_than_the_limit_is_not_started_and_is_recorded() {
on_a_deep_stack(|| {
let env = env();
let term = sum(500);
fuel::set_budget(None);
assert_eq!(Reducer::new().whnf_env(&term, &env), nat(500));
assert!(!fuel::reduction_limit_hit());
fuel::set_budget(None);
fuel::set_reduction_nesting_limit(Some(64));
let stuck = Reducer::new().whnf_env(&term, &env);
assert!(
!matches!(stuck, Expr::Lit(_)),
"a reduction 500 deep cannot finish 64 deep"
);
assert!(fuel::reduction_limit_hit());
assert_eq!(stuck, term);
fuel::set_budget(None);
assert!(!fuel::reduction_limit_hit());
assert_eq!(fuel::reduction_nesting_limit(), Some(64));
assert_eq!(Reducer::new().whnf_env(&sum(40), &env), nat(40));
assert!(!fuel::reduction_limit_hit());
fuel::set_reduction_nesting_limit(None);
assert_eq!(Reducer::new().whnf_env(&term, &env), nat(500));
});
}
#[test]
fn the_limit_counts_reductions_in_progress_not_reductions_made() {
on_a_deep_stack(|| {
let env = env();
fuel::set_budget(None);
fuel::set_reduction_nesting_limit(Some(16));
let mut reducer = Reducer::new();
for k in 0..2_000u64 {
let value = reducer.whnf_env(&app(app(cnst("Nat.add"), sum(8)), nat(k)), &env);
assert_eq!(value, nat(8 + k));
}
assert!(!fuel::reduction_limit_hit());
fuel::set_reduction_nesting_limit(None);
});
}
fn base_env() -> Environment {
let mut env = Environment::new();
let axiom = |env: &mut Environment, name: &str, ty: Expr| {
let ci = ConstantInfo::Axiom(AxiomVal {
common: ConstantVal {
name: Name::str(name),
level_params: vec![],
ty,
},
is_unsafe: false,
});
check_constant_info(env, ci).unwrap_or_else(|e| panic!("axiom {name}: {e:?}"));
};
axiom(&mut env, "N", Expr::Sort(Level::succ(Level::zero())));
axiom(
&mut env,
"g",
Expr::Pi(
BinderInfo::Default,
Name::str("x"),
Node::new(cnst("N")),
Node::new(cnst("N")),
),
);
axiom(&mut env, "a", cnst("N"));
env
}
fn chain_definition(name: &str, k: u32) -> ConstantInfo {
let mut value = cnst("a");
for _ in 0..k {
value = app(cnst("g"), value);
}
ConstantInfo::Definition(DefinitionVal {
common: ConstantVal {
name: Name::str(name),
level_params: vec![],
ty: cnst("N"),
},
value,
hints: ReducibilityHint::Regular(1),
safety: DefinitionSafety::Safe,
all: vec![Name::str(name)],
})
}
#[test]
fn a_declaration_with_a_term_over_the_depth_limit_is_refused_where_depth_is_tracked() {
on_a_deep_stack(|| {
let mut env = base_env();
fuel::set_budget(None);
check_constant_info(&mut env, chain_definition("deep", 1_000))
.unwrap_or_else(|e| panic!("{e:?}"));
fuel::set_term_depth_limit(Some(100));
fuel::set_budget(None);
check_constant_info(&mut env, chain_definition("within", 99))
.unwrap_or_else(|e| panic!("{e:?}"));
assert!(!fuel::term_depth_limit_hit());
fuel::set_budget(None);
let result = check_constant_info(&mut env, chain_definition("over", 100));
if fuel::TERM_DEPTH_TRACKED {
match result {
Err(KernelError::Other(message)) => assert!(
message.contains("term-depth limit"),
"expected a refusal for the depth, got: {message}"
),
other => panic!("expected a refusal for the depth, got {other:?}"),
}
assert!(fuel::term_depth_limit_hit() && fuel::is_exhausted());
assert!(env.find(&Name::str("over")).is_none());
} else {
result.unwrap_or_else(|e| panic!("{e:?}"));
assert!(!fuel::term_depth_limit_hit());
}
fuel::set_budget(None);
assert!(!fuel::term_depth_limit_hit() && !fuel::is_exhausted());
check_constant_info(&mut env, chain_definition("within_again", 50))
.unwrap_or_else(|e| panic!("{e:?}"));
fuel::set_term_depth_limit(None);
});
}