1use std::{collections::BTreeMap, sync::Arc};
4
5use sim_kernel::{
6 CapabilityName, CapabilitySet, Cx, Error, Expr, ReadPolicy, Result, Shape, ShapeId, Symbol,
7 TrustLevel, read_eval_capability,
8};
9use sim_shape::{expected_shape_diagnostic, parse_shape_expr};
10
11use super::{ReadEvalBroker, ReadEvalRequest, ReadEvalSource, RequestOrigin};
12
13#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct HostConfigEvalOptIn {
20 read_policy: ReadPolicy,
21}
22
23impl HostConfigEvalOptIn {
24 pub fn new(read_policy: ReadPolicy) -> Self {
26 Self { read_policy }
27 }
28
29 pub fn trusted(capabilities: CapabilitySet) -> Self {
32 Self::new(ReadPolicy {
33 trust: TrustLevel::TrustedSource,
34 capabilities: capabilities.grant(read_eval_capability()),
35 })
36 }
37
38 pub fn read_policy(&self) -> &ReadPolicy {
40 &self.read_policy
41 }
42}
43
44#[derive(Clone)]
46pub struct ConfigEvalNode {
47 pub codec: Symbol,
49 pub source: ReadEvalSource,
51 pub requires: Vec<CapabilityName>,
53 pub allow: CapabilitySet,
55 pub expected_shape: Arc<dyn Shape>,
57}
58
59impl ConfigEvalNode {
60 fn into_request(self, read_policy: ReadPolicy, detail: Expr) -> ReadEvalRequest {
61 ReadEvalRequest {
62 origin: RequestOrigin::with_detail(config_eval_origin_tag(), detail),
63 codec: self.codec,
64 source: self.source,
65 read_policy,
66 requires: self.requires,
67 allow: self.allow,
68 expected_shape: self.expected_shape,
69 }
70 }
71}
72
73pub fn config_eval_node_symbol() -> Symbol {
75 Symbol::qualified("config", "eval")
76}
77
78pub fn config_eval_origin_tag() -> Symbol {
80 Symbol::qualified("config", "node")
81}
82
83pub fn parse_config_eval_node(expr: &Expr) -> Result<ConfigEvalNode> {
90 let fields = match expr {
91 Expr::Map(entries) => collect_map_fields(entries)?,
92 Expr::List(items) => collect_list_fields(items)?,
93 _ => {
94 return Err(config_eval_error(
95 "config/eval node must be a field map or headed list",
96 ));
97 }
98 };
99 build_node(fields)
100}
101
102pub fn realize_config_expr(
109 cx: &mut Cx,
110 broker: &ReadEvalBroker,
111 opt_in: Option<&HostConfigEvalOptIn>,
112 expr: Expr,
113) -> Result<Expr> {
114 let Some(opt_in) = opt_in else {
115 return Ok(expr);
116 };
117 realize_expr_inner(cx, broker, opt_in, expr)
118}
119
120fn realize_expr_inner(
121 cx: &mut Cx,
122 broker: &ReadEvalBroker,
123 opt_in: &HostConfigEvalOptIn,
124 expr: Expr,
125) -> Result<Expr> {
126 match expr {
127 Expr::Map(entries) => realize_map(cx, broker, opt_in, entries),
128 Expr::List(items) if list_head_is_config_eval(&items) => {
129 admit_node_expr(cx, broker, opt_in, Expr::List(items))
130 }
131 Expr::List(items) => items
132 .into_iter()
133 .map(|item| realize_expr_inner(cx, broker, opt_in, item))
134 .collect::<Result<Vec<_>>>()
135 .map(Expr::List),
136 Expr::Vector(items) => items
137 .into_iter()
138 .map(|item| realize_expr_inner(cx, broker, opt_in, item))
139 .collect::<Result<Vec<_>>>()
140 .map(Expr::Vector),
141 Expr::Set(items) => items
142 .into_iter()
143 .map(|item| realize_expr_inner(cx, broker, opt_in, item))
144 .collect::<Result<Vec<_>>>()
145 .map(Expr::Set),
146 other => Ok(other),
147 }
148}
149
150fn realize_map(
151 cx: &mut Cx,
152 broker: &ReadEvalBroker,
153 opt_in: &HostConfigEvalOptIn,
154 entries: Vec<(Expr, Expr)>,
155) -> Result<Expr> {
156 let mut realized = Vec::<(Expr, Expr)>::new();
157 for (key, value) in entries {
158 if key_is_config_eval(&key) {
159 let merged = admit_node_expr(cx, broker, opt_in, value)?;
160 let Expr::Map(entries) = merged else {
161 return Err(config_eval_error(
162 "config/eval map entry must produce a map result",
163 ));
164 };
165 for entry in entries {
166 push_unique_entry(&mut realized, entry)?;
167 }
168 } else {
169 let value = realize_expr_inner(cx, broker, opt_in, value)?;
170 push_unique_entry(&mut realized, (key, value))?;
171 }
172 }
173 Ok(Expr::Map(realized))
174}
175
176fn admit_node_expr(
177 cx: &mut Cx,
178 broker: &ReadEvalBroker,
179 opt_in: &HostConfigEvalOptIn,
180 node_expr: Expr,
181) -> Result<Expr> {
182 let node = parse_config_eval_node(&node_expr)?;
183 let value = broker.admit(
184 cx,
185 node.into_request(opt_in.read_policy().clone(), node_expr),
186 )?;
187 value.object().as_expr(cx)
188}
189
190fn push_unique_entry(target: &mut Vec<(Expr, Expr)>, entry: (Expr, Expr)) -> Result<()> {
191 if target.iter().any(|(key, _)| key == &entry.0) {
192 return Err(config_eval_error(
193 "config/eval merge produced a duplicate key",
194 ));
195 }
196 target.push(entry);
197 Ok(())
198}
199
200fn build_node(fields: BTreeMap<String, &Expr>) -> Result<ConfigEvalNode> {
201 let codec = parse_symbol(required_field(&fields, "codec")?, "codec")?;
202 let requires = parse_capability_list(required_field(&fields, "requires")?, "requires")?;
203 let allow = parse_capability_set(required_field(&fields, "allow")?, "allow")?;
204 let expected_shape = parse_shape_field(required_field(&fields, "shape")?)?;
205 let source = match (fields.get("source"), fields.get("expr")) {
206 (Some(_), Some(_)) => {
207 return Err(config_eval_error(
208 "config/eval node must carry only one of source or expr",
209 ));
210 }
211 (Some(source), None) => parse_source_field(source)?,
212 (None, Some(expr)) => ReadEvalSource::Expr((*expr).clone()),
213 (None, None) => {
214 return Err(config_eval_error(
215 "config/eval node requires source or expr",
216 ));
217 }
218 };
219
220 Ok(ConfigEvalNode {
221 codec,
222 source,
223 requires,
224 allow,
225 expected_shape,
226 })
227}
228
229fn collect_map_fields(entries: &[(Expr, Expr)]) -> Result<BTreeMap<String, &Expr>> {
230 let mut fields = BTreeMap::new();
231 for (key, value) in entries {
232 let name = field_name(key)?;
233 if fields.insert(name.clone(), value).is_some() {
234 return Err(config_eval_error(format!(
235 "config/eval node repeats field {name:?}"
236 )));
237 }
238 }
239 Ok(fields)
240}
241
242fn collect_list_fields(items: &[Expr]) -> Result<BTreeMap<String, &Expr>> {
243 let Some((head, tail)) = items.split_first() else {
244 return Err(config_eval_error("config/eval list cannot be empty"));
245 };
246 if !expr_is_config_eval(head) {
247 return Err(config_eval_error("config/eval list has the wrong head"));
248 }
249 let tail = if matches!(tail.first(), Some(version) if expr_text(version) == Some("v1")) {
250 &tail[1..]
251 } else {
252 tail
253 };
254 if tail.len() % 2 != 0 {
255 return Err(config_eval_error(
256 "config/eval list fields must be key/value pairs",
257 ));
258 }
259 let mut fields = BTreeMap::new();
260 for pair in tail.chunks_exact(2) {
261 let name = field_name(&pair[0])?;
262 if fields.insert(name.clone(), &pair[1]).is_some() {
263 return Err(config_eval_error(format!(
264 "config/eval node repeats field {name:?}"
265 )));
266 }
267 }
268 Ok(fields)
269}
270
271fn required_field<'a>(fields: &'a BTreeMap<String, &Expr>, name: &str) -> Result<&'a Expr> {
272 fields
273 .get(name)
274 .copied()
275 .ok_or_else(|| config_eval_error(format!("config/eval node requires {name:?}")))
276}
277
278fn parse_source_field(expr: &Expr) -> Result<ReadEvalSource> {
279 match expr {
280 Expr::String(text) => Ok(ReadEvalSource::Text(text.clone())),
281 Expr::Bytes(bytes) => Ok(ReadEvalSource::Bytes(bytes.clone())),
282 _ => Err(config_eval_error("source must be a string or bytes value")),
283 }
284}
285
286fn parse_shape_field(expr: &Expr) -> Result<Arc<dyn Shape>> {
287 match expr {
288 Expr::String(text) => parse_shape_expr(&Expr::Symbol(parse_symbol_text(text))),
289 Expr::Symbol(_) | Expr::List(_) => parse_shape_expr(expr),
290 _ => Err(Error::WrongShape {
291 expected: ShapeId(0),
292 diagnostics: vec![expected_shape_diagnostic(
293 "shape expression",
294 "config/eval shape",
295 )],
296 }),
297 }
298}
299
300fn parse_capability_list(expr: &Expr, field: &str) -> Result<Vec<CapabilityName>> {
301 capability_items(expr, field)?
302 .iter()
303 .map(parse_capability)
304 .collect()
305}
306
307fn parse_capability_set(expr: &Expr, field: &str) -> Result<CapabilitySet> {
308 parse_capability_list(expr, field).map(|capabilities| {
309 capabilities
310 .into_iter()
311 .fold(CapabilitySet::new(), CapabilitySet::grant)
312 })
313}
314
315fn capability_items<'a>(expr: &'a Expr, field: &str) -> Result<&'a [Expr]> {
316 match expr {
317 Expr::List(items) | Expr::Vector(items) | Expr::Set(items) => Ok(items),
318 _ => Err(config_eval_error(format!(
319 "{field} must be a list of capability names"
320 ))),
321 }
322}
323
324fn parse_capability(expr: &Expr) -> Result<CapabilityName> {
325 let text = match expr {
326 Expr::String(text) => text.clone(),
327 Expr::Symbol(symbol) => symbol_text(symbol),
328 _ => {
329 return Err(config_eval_error(
330 "capability entries must be strings or symbols",
331 ));
332 }
333 };
334 let text = strip_field_prefix(&text);
335 let text = text.strip_prefix("capability/").unwrap_or(text).to_owned();
336 Ok(CapabilityName::new(text))
337}
338
339fn parse_symbol(expr: &Expr, field: &str) -> Result<Symbol> {
340 match expr {
341 Expr::String(text) => Ok(parse_symbol_text(text)),
342 Expr::Symbol(symbol) => Ok(symbol.clone()),
343 _ => Err(config_eval_error(format!(
344 "{field} must be a string or symbol"
345 ))),
346 }
347}
348
349fn parse_symbol_text(text: &str) -> Symbol {
350 let text = strip_field_prefix(text);
351 match text.split_once('/') {
352 Some((namespace, name)) if !namespace.is_empty() && !name.is_empty() => {
353 Symbol::qualified(namespace.to_owned(), name.to_owned())
354 }
355 _ => Symbol::new(text.to_owned()),
356 }
357}
358
359fn field_name(expr: &Expr) -> Result<String> {
360 match expr {
361 Expr::String(text) => Ok(strip_field_prefix(text).to_owned()),
362 Expr::Symbol(symbol) => Ok(strip_field_prefix(&symbol_text(symbol)).to_owned()),
363 _ => Err(config_eval_error(
364 "config/eval field names must be strings or symbols",
365 )),
366 }
367}
368
369fn list_head_is_config_eval(items: &[Expr]) -> bool {
370 items.first().is_some_and(expr_is_config_eval)
371}
372
373fn key_is_config_eval(expr: &Expr) -> bool {
374 expr_is_config_eval(expr)
375}
376
377fn expr_is_config_eval(expr: &Expr) -> bool {
378 match expr {
379 Expr::Symbol(symbol) => symbol_is_config_eval(symbol),
380 Expr::String(text) => text == "config/eval",
381 _ => false,
382 }
383}
384
385fn symbol_is_config_eval(symbol: &Symbol) -> bool {
386 symbol == &config_eval_node_symbol()
387 || (symbol.namespace.is_none() && symbol.name.as_ref() == "config/eval")
388}
389
390fn expr_text(expr: &Expr) -> Option<&str> {
391 match expr {
392 Expr::String(text) => Some(text),
393 Expr::Symbol(symbol) if symbol.namespace.is_none() => Some(symbol.name.as_ref()),
394 _ => None,
395 }
396}
397
398fn symbol_text(symbol: &Symbol) -> String {
399 match &symbol.namespace {
400 Some(namespace) => format!("{namespace}/{}", symbol.name),
401 None => symbol.name.to_string(),
402 }
403}
404
405fn strip_field_prefix(text: &str) -> &str {
406 text.strip_prefix(':').unwrap_or(text)
407}
408
409fn config_eval_error(message: impl Into<String>) -> Error {
410 Error::domain_error(
411 config_eval_node_symbol(),
412 Symbol::qualified("config", "eval-node"),
413 message,
414 )
415}