Skip to main content

sim_lib_cookbook/
run.rs

1//! The `cookbook:run` algorithm: decode a recipe's setup through its declared
2//! codec, evaluate it, encode the result, and check declared expectations.
3//!
4//! Per the design (Section 7) this is capability-gated: running a recipe is
5//! read-eval, so the runtime must hold the read-eval capability. Listing and
6//! showing recipes is not gated.
7//!
8//! First cut: the setup decodes to a single top-level form (form 0). The data
9//! model already carries a `Vec` of results and expectations, so multi-form
10//! setups are a forward-compatible extension.
11
12use sim_codec::{Input, decode_with_codec, encode_value_with_codec};
13use sim_cookbook::{CheckResult, RecipeCard, RecipeRun};
14use sim_kernel::{Cx, EncodeOptions, Error, ReadPolicy, Result, Symbol, read_eval_capability};
15
16/// Error unless the runtime holds the read-eval capability.
17pub fn require_eval_capability(cx: &Cx) -> Result<()> {
18    if cx.capabilities().contains(&read_eval_capability()) {
19        Ok(())
20    } else {
21        Err(Error::CapabilityDenied {
22            capability: read_eval_capability(),
23        })
24    }
25}
26
27/// Lib ids in `card.requires` that are not currently loaded.
28///
29/// A requirement matches a loaded lib by its fully qualified id
30/// (`namespace/name`) or by its unqualified `name` alone, so `numbers-f64`
31/// matches a lib whose id is `lisp/numbers-f64`.
32pub fn missing_requires(cx: &Cx, card: &RecipeCard) -> Vec<String> {
33    let loaded: Vec<(String, String)> = cx
34        .registry()
35        .libs()
36        .iter()
37        .map(|lib| {
38            (
39                lib.manifest.id.as_qualified_str(),
40                lib.manifest.id.name.to_string(),
41            )
42        })
43        .collect();
44    card.requires
45        .iter()
46        .filter(|req| {
47            !loaded
48                .iter()
49                .any(|(qualified, name)| qualified == *req || name == *req)
50        })
51        .cloned()
52        .collect()
53}
54
55/// Run a recipe end to end. Hard errors (missing requires, unknown codec,
56/// undecodable setup) return `Err`; an evaluation error is captured as
57/// `ok == false` with empty results so the caller still sees a `RecipeRun`.
58pub fn run_recipe(cx: &mut Cx, card: &RecipeCard) -> Result<RecipeRun> {
59    require_eval_capability(cx)?;
60
61    let missing = missing_requires(cx, card);
62    if !missing.is_empty() {
63        return Err(Error::Eval(format!(
64            "recipe {} requires libs not loaded: {}",
65            card.id,
66            missing.join(", ")
67        )));
68    }
69
70    let codec = Symbol::qualified("codec", card.codec.as_str());
71    let source = String::from_utf8(card.setup.clone())
72        .map_err(|e| Error::Eval(format!("recipe {} setup is not UTF-8: {e}", card.id)))?;
73    let expr = decode_with_codec(cx, &codec, Input::Text(source), ReadPolicy::default())?;
74
75    let (results, eval_ok) = match cx.eval_expr(expr) {
76        Ok(value) => {
77            let text = encode_value_with_codec(cx, &codec, &value, EncodeOptions::default())?
78                .into_text()?;
79            (vec![text], true)
80        }
81        Err(_) => (Vec::new(), false),
82    };
83
84    let mut checks = Vec::new();
85    let mut all_pass = true;
86    for expectation in &card.expect {
87        let actual = results.get(expectation.form).cloned();
88        let pass = actual.as_deref() == Some(expectation.result.as_str());
89        if !pass {
90            all_pass = false;
91        }
92        checks.push(CheckResult {
93            form: expectation.form,
94            expected: expectation.result.clone(),
95            actual: actual.unwrap_or_else(|| "<no such form>".to_string()),
96            pass,
97        });
98    }
99
100    Ok(RecipeRun {
101        recipe: card.id.clone(),
102        forms: results.len(),
103        results,
104        ok: eval_ok && all_pass,
105        checks,
106    })
107}
108
109/// Decode a recipe's setup to an `Expr` without evaluating it (`cookbook:setup`).
110pub fn decode_setup(cx: &mut Cx, card: &RecipeCard) -> Result<sim_kernel::Expr> {
111    let codec = Symbol::qualified("codec", card.codec.as_str());
112    let source = String::from_utf8(card.setup.clone())
113        .map_err(|e| Error::Eval(format!("recipe {} setup is not UTF-8: {e}", card.id)))?;
114    decode_with_codec(cx, &codec, Input::Text(source), ReadPolicy::default())
115}