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, 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()
}
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 = decode_eval_expr_with_codec(cx, &codec, Input::Text(source), read_policy)?;
let (results, eval_ok) = match cx.eval_expr(expr) {
Ok(value) => {
let text = encode_value_with_codec(cx, &codec, &value, EncodeOptions::default())?
.into_text()?;
(vec![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())
}