use sim_codec::{Input, decode_eval_expr_with_codec, decode_with_codec, encode_value_with_codec};
use sim_cookbook::{CheckResult, RecipeCard, RecipeRun};
use sim_kernel::{
CapabilitySet, Cx, EncodeOptions, Error, Expr, ReadPolicy, Result, Symbol, TrustLevel,
read_construct_capability, read_eval_capability,
};
use crate::catalog::{EmptyCatalog, LibCatalog, load_requires};
pub fn require_eval_capability(cx: &Cx) -> Result<()> {
if cx.capabilities().contains(&read_eval_capability()) {
Ok(())
} else {
Err(Error::CapabilityDenied {
capability: read_eval_capability(),
})
}
}
pub fn missing_requires(cx: &Cx, card: &RecipeCard) -> Vec<String> {
let loaded: Vec<(String, String)> = cx
.registry()
.libs()
.iter()
.map(|lib| {
(
lib.manifest.id.as_qualified_str(),
lib.manifest.id.name.to_string(),
)
})
.collect();
card.requires
.iter()
.filter(|req| {
!loaded
.iter()
.any(|(qualified, name)| qualified == *req || name == *req)
})
.cloned()
.collect()
}
fn resolve_operators(expr: Expr) -> Expr {
match expr {
Expr::Infix {
operator,
left,
right,
} => Expr::Call {
operator: Box::new(Expr::Symbol(operator)),
args: vec![resolve_operators(*left), resolve_operators(*right)],
},
Expr::Prefix { operator, arg } | Expr::Postfix { operator, arg } => Expr::Call {
operator: Box::new(Expr::Symbol(operator)),
args: vec![resolve_operators(*arg)],
},
Expr::Call { operator, args } => Expr::Call {
operator: Box::new(resolve_operators(*operator)),
args: args.into_iter().map(resolve_operators).collect(),
},
Expr::List(items) => Expr::List(items.into_iter().map(resolve_operators).collect()),
Expr::Vector(items) => Expr::Vector(items.into_iter().map(resolve_operators).collect()),
Expr::Set(items) => Expr::Set(items.into_iter().map(resolve_operators).collect()),
Expr::Block(items) => Expr::Block(items.into_iter().map(resolve_operators).collect()),
Expr::Map(entries) => Expr::Map(
entries
.into_iter()
.map(|(k, v)| (resolve_operators(k), resolve_operators(v)))
.collect(),
),
Expr::Annotated { expr, annotations } => Expr::Annotated {
expr: Box::new(resolve_operators(*expr)),
annotations,
},
other => other,
}
}
pub fn run_recipe(cx: &mut Cx, card: &RecipeCard) -> Result<RecipeRun> {
run_recipe_with_catalog(cx, &EmptyCatalog, card)
}
pub fn run_recipe_with_catalog(
cx: &mut Cx,
catalog: &dyn LibCatalog,
card: &RecipeCard,
) -> Result<RecipeRun> {
require_eval_capability(cx)?;
let unresolved = load_requires(cx, catalog, card);
if !unresolved.is_empty() {
return Err(Error::Eval(format!(
"recipe {} descriptor: requires not in catalog: {}",
card.id,
unresolved.join(", ")
)));
}
let codec = Symbol::qualified("codec", card.codec.as_str());
let source = String::from_utf8(card.setup.clone())
.map_err(|e| Error::Eval(format!("recipe {} setup is not UTF-8: {e}", card.id)))?;
let read_policy = ReadPolicy {
trust: TrustLevel::TrustedSource,
capabilities: CapabilitySet::new()
.grant(read_construct_capability())
.grant(read_eval_capability()),
};
let expr = resolve_operators(decode_eval_expr_with_codec(
cx,
&codec,
Input::Text(source),
read_policy,
)?);
let (results, eval_ok) = match cx.eval_expr(expr) {
Ok(value) => {
let encoded = encode_value_with_codec(cx, &codec, &value, EncodeOptions::default())
.or_else(|_| {
let lisp = Symbol::qualified("codec", "lisp");
encode_value_with_codec(cx, &lisp, &value, EncodeOptions::default())
})?;
(vec![encoded.into_text()?], true)
}
Err(_) => (Vec::new(), false),
};
let mut checks = Vec::new();
let mut all_pass = true;
for expectation in &card.expect {
let actual = results.get(expectation.form).cloned();
let pass = actual.as_deref() == Some(expectation.result.as_str());
if !pass {
all_pass = false;
}
checks.push(CheckResult {
form: expectation.form,
expected: expectation.result.clone(),
actual: actual.unwrap_or_else(|| "<no such form>".to_string()),
pass,
});
}
Ok(RecipeRun {
recipe: card.id.clone(),
forms: results.len(),
results,
ok: eval_ok && all_pass,
checks,
})
}
pub fn run_recipe_twice(
cx: &mut Cx,
catalog: &dyn LibCatalog,
card: &RecipeCard,
) -> Result<RecipeRun> {
let first = run_recipe_with_catalog(cx, catalog, card)?;
let second = run_recipe_with_catalog(cx, catalog, card)?;
if first.results != second.results {
return Err(Error::Eval(format!(
"recipe {} is not deterministic: {:?} != {:?}",
card.id, first.results, second.results
)));
}
Ok(first)
}
pub fn decode_setup(cx: &mut Cx, card: &RecipeCard) -> Result<sim_kernel::Expr> {
let codec = Symbol::qualified("codec", card.codec.as_str());
let source = String::from_utf8(card.setup.clone())
.map_err(|e| Error::Eval(format!("recipe {} setup is not UTF-8: {e}", card.id)))?;
decode_with_codec(cx, &codec, Input::Text(source), ReadPolicy::default())
}