1use std::sync::Arc;
12
13use sim_codec::{
14 Input, decode_eval_expr_with_codec, decode_with_codec, encode_value_with_codec,
15 lower_operator_nodes,
16};
17use sim_cookbook::{CheckResult, RecipeCard, RecipeRun};
18use sim_kernel::{
19 CapabilityName, CapabilitySet, Cx, EncodeOptions, Error, Expr, ReadPolicy, Result, Shape,
20 Symbol, TrustLevel, read_construct_capability, read_eval_capability,
21};
22use sim_lib_core::{ReadEvalBroker, ReadEvalRequest, ReadEvalSource, RequestOrigin};
23use sim_shape::AnyShape;
24
25use crate::catalog::{CookbookCapabilityProfile, EmptyCatalog, LibCatalog, load_requires};
26
27pub fn require_eval_capability(cx: &Cx) -> Result<()> {
29 if cx.capabilities().contains(&read_eval_capability()) {
30 Ok(())
31 } else {
32 Err(Error::CapabilityDenied {
33 capability: read_eval_capability(),
34 })
35 }
36}
37
38pub fn missing_requires(cx: &Cx, card: &RecipeCard) -> Vec<String> {
44 let loaded: Vec<(String, String)> = cx
45 .registry()
46 .libs()
47 .iter()
48 .map(|lib| {
49 (
50 lib.manifest.id.as_qualified_str(),
51 lib.manifest.id.name.to_string(),
52 )
53 })
54 .collect();
55 card.requires
56 .iter()
57 .filter(|req| {
58 !loaded
59 .iter()
60 .any(|(qualified, name)| qualified == *req || name == *req)
61 })
62 .cloned()
63 .collect()
64}
65
66pub fn run_recipe(cx: &mut Cx, card: &RecipeCard) -> Result<RecipeRun> {
73 run_recipe_with_catalog(cx, &EmptyCatalog, card)
74}
75
76pub fn run_recipe_with_catalog(
85 cx: &mut Cx,
86 catalog: &dyn LibCatalog,
87 card: &RecipeCard,
88) -> Result<RecipeRun> {
89 run_recipe_with_catalog_shape(cx, catalog, card, recipe_result_shape())
90}
91
92fn run_recipe_with_catalog_shape(
93 cx: &mut Cx,
94 catalog: &dyn LibCatalog,
95 card: &RecipeCard,
96 expected_shape: Arc<dyn Shape>,
97) -> Result<RecipeRun> {
98 require_eval_capability(cx)?;
99
100 let unresolved = load_requires(cx, catalog, card);
101 if !unresolved.is_empty() {
102 return Err(Error::Eval(format!(
103 "recipe {} descriptor: requires not in catalog: {}",
104 card.id,
105 unresolved.join(", ")
106 )));
107 }
108
109 let codec = Symbol::qualified("codec", card.codec.as_str());
110 let source = String::from_utf8(card.setup.clone())
111 .map_err(|e| Error::Eval(format!("recipe {} setup is not UTF-8: {e}", card.id)))?;
112 let expr = lower_operator_nodes(decode_eval_expr_with_codec(
116 cx,
117 &codec,
118 Input::Text(source),
119 trusted_recipe_read_policy(),
120 )?);
121
122 let request = recipe_read_eval_request(
123 card,
124 codec.clone(),
125 ReadEvalSource::Expr(expr),
126 expected_shape,
127 );
128 let broker = ReadEvalBroker::new();
129 let (results, eval_ok) = match broker.admit(cx, request) {
130 Ok(value) => {
131 let encoded = encode_value_with_codec(cx, &codec, &value, EncodeOptions::default())
138 .or_else(|_| {
139 let lisp = Symbol::qualified("codec", "lisp");
140 encode_value_with_codec(cx, &lisp, &value, EncodeOptions::default())
141 })?;
142 (vec![encoded.into_text()?], true)
143 }
144 Err(err) if is_hard_broker_error(&err) => return Err(err),
145 Err(_) => (Vec::new(), false),
146 };
147
148 let mut checks = Vec::new();
149 let mut all_pass = true;
150 for expectation in &card.expect {
151 let actual = results.get(expectation.form).cloned();
152 let pass = actual.as_deref() == Some(expectation.result.as_str());
153 if !pass {
154 all_pass = false;
155 }
156 checks.push(CheckResult {
157 form: expectation.form,
158 expected: expectation.result.clone(),
159 actual: actual.unwrap_or_else(|| "<no such form>".to_string()),
160 pass,
161 });
162 }
163
164 Ok(RecipeRun {
165 recipe: card.id.clone(),
166 forms: results.len(),
167 results,
168 ok: eval_ok && all_pass,
169 checks,
170 })
171}
172
173#[cfg(test)]
174pub(crate) fn run_recipe_with_catalog_for_shape_test(
175 cx: &mut Cx,
176 catalog: &dyn LibCatalog,
177 card: &RecipeCard,
178 expected_shape: Arc<dyn Shape>,
179) -> Result<RecipeRun> {
180 run_recipe_with_catalog_shape(cx, catalog, card, expected_shape)
181}
182
183fn recipe_read_eval_request(
184 card: &RecipeCard,
185 codec: Symbol,
186 source: ReadEvalSource,
187 expected_shape: Arc<dyn Shape>,
188) -> ReadEvalRequest {
189 ReadEvalRequest {
190 origin: RequestOrigin::with_detail(
191 Symbol::qualified("cookbook", "recipe"),
192 Expr::String(card.id.clone()),
193 ),
194 codec,
195 source,
196 read_policy: trusted_recipe_read_policy(),
197 requires: recipe_required_capabilities(card),
198 allow: recipe_allowed_capabilities(card),
199 expected_shape,
200 }
201}
202
203fn recipe_result_shape() -> Arc<dyn Shape> {
204 Arc::new(AnyShape)
205}
206
207fn trusted_recipe_read_policy() -> ReadPolicy {
208 ReadPolicy {
209 trust: TrustLevel::TrustedSource,
210 capabilities: CapabilitySet::new()
211 .grant(read_construct_capability())
212 .grant(read_eval_capability()),
213 }
214}
215
216fn recipe_required_capabilities(card: &RecipeCard) -> Vec<CapabilityName> {
217 let mut capabilities = vec![read_eval_capability()];
218 capabilities.extend(tagged_capabilities(card, "requires-capability:"));
219 sort_dedup_capabilities(&mut capabilities);
220 capabilities
221}
222
223fn recipe_allowed_capabilities(card: &RecipeCard) -> CapabilitySet {
224 let mut capabilities = tagged_capabilities(card, "allow-capability:");
225 if capabilities.is_empty() {
226 capabilities = CookbookCapabilityProfile::granted();
227 }
228 capabilities.extend(recipe_required_capabilities(card));
229 capabilities.push(read_construct_capability());
230 sort_dedup_capabilities(&mut capabilities);
231 capabilities
232 .into_iter()
233 .fold(CapabilitySet::new(), CapabilitySet::grant)
234}
235
236fn tagged_capabilities(card: &RecipeCard, prefix: &str) -> Vec<CapabilityName> {
237 card.tags
238 .iter()
239 .filter_map(|tag| {
240 let name = tag.strip_prefix(prefix)?;
241 (!name.is_empty()).then(|| CapabilityName::new(name.to_owned()))
242 })
243 .collect()
244}
245
246fn sort_dedup_capabilities(capabilities: &mut Vec<CapabilityName>) {
247 capabilities.sort();
248 capabilities.dedup();
249}
250
251fn is_hard_broker_error(err: &Error) -> bool {
252 matches!(
253 err,
254 Error::CapabilityDenied { .. }
255 | Error::TrustDenied { .. }
256 | Error::WrongShape { .. }
257 | Error::CodecError { .. }
258 )
259}
260
261pub fn run_recipe_twice(
271 cx: &mut Cx,
272 catalog: &dyn LibCatalog,
273 card: &RecipeCard,
274) -> Result<RecipeRun> {
275 let first = run_recipe_with_catalog(cx, catalog, card)?;
276 let second = run_recipe_with_catalog(cx, catalog, card)?;
277 if first.results != second.results {
278 return Err(Error::Eval(format!(
279 "recipe {} is not deterministic: {:?} != {:?}",
280 card.id, first.results, second.results
281 )));
282 }
283 Ok(first)
284}
285
286pub fn decode_setup(cx: &mut Cx, card: &RecipeCard) -> Result<sim_kernel::Expr> {
288 let codec = Symbol::qualified("codec", card.codec.as_str());
289 let source = String::from_utf8(card.setup.clone())
290 .map_err(|e| Error::Eval(format!("recipe {} setup is not UTF-8: {e}", card.id)))?;
291 decode_with_codec(cx, &codec, Input::Text(source), ReadPolicy::default())
292}