1use sim_codec::{Input, decode_term_with_codec, decode_with_codec, encode_value_with_codec};
13use sim_cookbook::{CheckResult, RecipeCard, RecipeRun};
14use sim_kernel::{
15 CapabilitySet, Cx, EncodeOptions, Error, Expr, ReadPolicy, Result, Symbol, TrustLevel,
16 read_construct_capability, read_eval_capability,
17};
18
19pub fn require_eval_capability(cx: &Cx) -> Result<()> {
21 if cx.capabilities().contains(&read_eval_capability()) {
22 Ok(())
23 } else {
24 Err(Error::CapabilityDenied {
25 capability: read_eval_capability(),
26 })
27 }
28}
29
30pub fn missing_requires(cx: &Cx, card: &RecipeCard) -> Vec<String> {
36 let loaded: Vec<(String, String)> = cx
37 .registry()
38 .libs()
39 .iter()
40 .map(|lib| {
41 (
42 lib.manifest.id.as_qualified_str(),
43 lib.manifest.id.name.to_string(),
44 )
45 })
46 .collect();
47 card.requires
48 .iter()
49 .filter(|req| {
50 !loaded
51 .iter()
52 .any(|(qualified, name)| qualified == *req || name == *req)
53 })
54 .cloned()
55 .collect()
56}
57
58pub fn run_recipe(cx: &mut Cx, card: &RecipeCard) -> Result<RecipeRun> {
62 require_eval_capability(cx)?;
63
64 let missing = missing_requires(cx, card);
65 if !missing.is_empty() {
66 return Err(Error::Eval(format!(
67 "recipe {} requires libs not loaded: {}",
68 card.id,
69 missing.join(", ")
70 )));
71 }
72
73 let codec = Symbol::qualified("codec", card.codec.as_str());
74 let source = String::from_utf8(card.setup.clone())
75 .map_err(|e| Error::Eval(format!("recipe {} setup is not UTF-8: {e}", card.id)))?;
76 let read_policy = ReadPolicy {
87 trust: TrustLevel::TrustedSource,
88 capabilities: CapabilitySet::new()
89 .grant(read_construct_capability())
90 .grant(read_eval_capability()),
91 };
92 let expr = Expr::from(decode_term_with_codec(
93 cx,
94 &codec,
95 Input::Text(source),
96 read_policy,
97 )?);
98
99 let (results, eval_ok) = match cx.eval_expr(expr) {
100 Ok(value) => {
101 let text = encode_value_with_codec(cx, &codec, &value, EncodeOptions::default())?
102 .into_text()?;
103 (vec![text], true)
104 }
105 Err(_) => (Vec::new(), false),
106 };
107
108 let mut checks = Vec::new();
109 let mut all_pass = true;
110 for expectation in &card.expect {
111 let actual = results.get(expectation.form).cloned();
112 let pass = actual.as_deref() == Some(expectation.result.as_str());
113 if !pass {
114 all_pass = false;
115 }
116 checks.push(CheckResult {
117 form: expectation.form,
118 expected: expectation.result.clone(),
119 actual: actual.unwrap_or_else(|| "<no such form>".to_string()),
120 pass,
121 });
122 }
123
124 Ok(RecipeRun {
125 recipe: card.id.clone(),
126 forms: results.len(),
127 results,
128 ok: eval_ok && all_pass,
129 checks,
130 })
131}
132
133pub fn decode_setup(cx: &mut Cx, card: &RecipeCard) -> Result<sim_kernel::Expr> {
135 let codec = Symbol::qualified("codec", card.codec.as_str());
136 let source = String::from_utf8(card.setup.clone())
137 .map_err(|e| Error::Eval(format!("recipe {} setup is not UTF-8: {e}", card.id)))?;
138 decode_with_codec(cx, &codec, Input::Text(source), ReadPolicy::default())
139}