1use std::collections::{BTreeMap, BTreeSet};
2
3use sim_kernel::{Error, Expr, Result, Symbol};
4use sim_value::build::entry;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum FrameKind {
9 Use,
11 Inform,
13 Task,
15 Require,
17 Forbid,
19 Prefer,
21 Return,
23 Check,
25}
26
27impl FrameKind {
28 pub fn prefix(self) -> &'static str {
30 match self {
31 Self::Use => "Use ",
32 Self::Inform => "Note ",
33 Self::Task => "You MUST ",
34 Self::Require => "You MUST ",
35 Self::Forbid => "You must NEVER ",
36 Self::Prefer => "Prefer to ",
37 Self::Return => "Return ",
38 Self::Check => "Check ",
39 }
40 }
41
42 pub fn grammar_priority(self) -> u8 {
44 match self {
45 Self::Require | Self::Forbid => 0,
46 Self::Task => 1,
47 Self::Return | Self::Check => 2,
48 Self::Use | Self::Prefer => 3,
49 Self::Inform => 4,
50 }
51 }
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub enum FrameHoleKind {
57 Ref,
59 Term,
61 Choice,
63 Path,
65 Number,
67 Prose,
69}
70
71impl FrameHoleKind {
72 pub fn shape_expr(self) -> Expr {
74 let shape = match self {
75 Self::Ref | Self::Term | Self::Choice | Self::Prose => {
76 Symbol::qualified("core", "String")
77 }
78 Self::Path => Symbol::qualified("core", "List"),
79 Self::Number => Symbol::qualified("core", "Number"),
80 };
81 Expr::Map(vec![
82 entry("hole-kind", Expr::Symbol(self.symbol())),
83 entry("shape", Expr::Symbol(shape)),
84 ])
85 }
86
87 fn symbol(self) -> Symbol {
88 match self {
89 Self::Ref => Symbol::qualified("bridge", "Ref"),
90 Self::Term => Symbol::qualified("bridge", "Term"),
91 Self::Choice => Symbol::qualified("bridge", "Choice"),
92 Self::Path => Symbol::qualified("bridge", "Path"),
93 Self::Number => Symbol::qualified("bridge", "Number"),
94 Self::Prose => Symbol::qualified("bridge", "Prose"),
95 }
96 }
97}
98
99#[derive(Clone, Debug, PartialEq, Eq)]
101pub struct FrameHoleSpec {
102 pub name: Symbol,
104 pub kind: FrameHoleKind,
106}
107
108impl FrameHoleSpec {
109 pub fn new(name: Symbol, kind: FrameHoleKind) -> Self {
111 Self { name, kind }
112 }
113}
114
115#[derive(Clone, Debug, PartialEq, Eq)]
117pub struct FrameSpec {
118 pub id: Symbol,
120 pub kind: FrameKind,
122 pub prefix: &'static str,
124 pub template: &'static str,
126 pub holes: Vec<FrameHoleSpec>,
128 pub grammar_priority: u8,
130}
131
132impl FrameSpec {
133 pub fn new(
135 id: Symbol,
136 kind: FrameKind,
137 template: &'static str,
138 holes: Vec<FrameHoleSpec>,
139 ) -> Self {
140 Self {
141 id,
142 kind,
143 prefix: kind.prefix(),
144 template,
145 holes,
146 grammar_priority: kind.grammar_priority(),
147 }
148 }
149}
150
151#[derive(Clone, Debug, PartialEq, Eq)]
153pub struct BridgeFramePayload {
154 pub frame: Symbol,
156 pub slots: BTreeMap<Symbol, Expr>,
158}
159
160impl BridgeFramePayload {
161 pub fn new(frame: Symbol) -> Self {
163 Self {
164 frame,
165 slots: BTreeMap::new(),
166 }
167 }
168
169 pub fn with_slot(mut self, name: Symbol, value: Expr) -> Self {
171 self.slots.insert(name, value);
172 self
173 }
174
175 pub fn from_expr(expr: &Expr) -> Result<Self> {
177 let fields = map_fields(expr, "bridge/Frame payload")?;
178 reject_unknown(fields, &["frame", "slots"])?;
179 Ok(Self {
180 frame: required_symbol(fields, "frame")?,
181 slots: optional_slots(fields, "slots")?,
182 })
183 }
184
185 pub fn to_expr(&self) -> Expr {
187 let mut fields = vec![entry("frame", Expr::Symbol(self.frame.clone()))];
188 if !self.slots.is_empty() {
189 fields.push(entry(
190 "slots",
191 Expr::Map(
192 self.slots
193 .iter()
194 .map(|(name, value)| (Expr::Symbol(name.clone()), value.clone()))
195 .collect(),
196 ),
197 ));
198 }
199 Expr::Map(fields)
200 }
201}
202
203#[derive(Clone, Debug, Default, PartialEq, Eq)]
205pub struct BridgeFrameBook {
206 specs: BTreeMap<Symbol, FrameSpec>,
207}
208
209impl BridgeFrameBook {
210 pub fn new() -> Self {
212 Self::default()
213 }
214
215 pub fn register(&mut self, spec: FrameSpec) {
217 self.specs.insert(spec.id.clone(), spec);
218 }
219
220 pub fn spec(&self, id: &Symbol) -> Option<&FrameSpec> {
222 self.specs.get(id)
223 }
224
225 pub fn require_spec(&self, id: &Symbol) -> Result<&FrameSpec> {
227 self.spec(id)
228 .ok_or_else(|| Error::Eval(format!("unknown BRIDGE frame {id}")))
229 }
230
231 pub fn specs(&self) -> impl Iterator<Item = &FrameSpec> {
233 self.specs.values()
234 }
235
236 pub fn validate_payload(&self, payload: &Expr) -> Result<BridgeFramePayload> {
238 let payload = BridgeFramePayload::from_expr(payload)?;
239 let spec = self.require_spec(&payload.frame)?;
240 validate_frame_payload(spec, &payload)?;
241 Ok(payload)
242 }
243}
244
245pub fn standard_frame_book() -> BridgeFrameBook {
247 let mut book = BridgeFrameBook::new();
248 for spec in [
249 frame(
250 "use",
251 FrameKind::Use,
252 "the referenced context {resource}.",
253 &[("resource", FrameHoleKind::Ref)],
254 ),
255 frame(
256 "inform",
257 FrameKind::Inform,
258 "{fact}.",
259 &[("fact", FrameHoleKind::Term)],
260 ),
261 frame(
262 "proposal",
263 FrameKind::Task,
264 "produce the requested proposal.",
265 &[],
266 ),
267 frame(
268 "answer",
269 FrameKind::Inform,
270 "answer the parent packet.",
271 &[],
272 ),
273 frame(
274 "produce-artifact",
275 FrameKind::Task,
276 "produce {what} for {target}.",
277 &[
278 ("what", FrameHoleKind::Term),
279 ("target", FrameHoleKind::Term),
280 ],
281 ),
282 frame(
283 "require",
284 FrameKind::Require,
285 "{rule}.",
286 &[("rule", FrameHoleKind::Term)],
287 ),
288 frame(
289 "forbid",
290 FrameKind::Forbid,
291 "{rule}.",
292 &[("rule", FrameHoleKind::Term)],
293 ),
294 frame(
295 "prefer",
296 FrameKind::Prefer,
297 "{choice}.",
298 &[("choice", FrameHoleKind::Choice)],
299 ),
300 frame(
301 "return",
302 FrameKind::Return,
303 "{shape}.",
304 &[("shape", FrameHoleKind::Term)],
305 ),
306 frame(
307 "check",
308 FrameKind::Check,
309 "{path}.",
310 &[("path", FrameHoleKind::Path)],
311 ),
312 frame(
313 "explain",
314 FrameKind::Inform,
315 "{text}.",
316 &[("text", FrameHoleKind::Prose)],
317 ),
318 ] {
319 book.register(spec);
320 }
321 book
322}
323
324pub(crate) fn validate_frame_payload(spec: &FrameSpec, payload: &BridgeFramePayload) -> Result<()> {
325 let expected = spec
326 .holes
327 .iter()
328 .map(|hole| hole.name.clone())
329 .collect::<BTreeSet<_>>();
330 for name in payload.slots.keys() {
331 if !expected.contains(name) {
332 return Err(Error::Eval(format!(
333 "BRIDGE frame {} has unknown hole {}",
334 spec.id, name
335 )));
336 }
337 }
338 for hole in &spec.holes {
339 let value = payload.slots.get(&hole.name).ok_or_else(|| {
340 Error::Eval(format!(
341 "BRIDGE frame {} missing hole {}",
342 spec.id, hole.name
343 ))
344 })?;
345 validate_hole(hole, value)?;
346 }
347 Ok(())
348}
349
350fn validate_hole(hole: &FrameHoleSpec, value: &Expr) -> Result<()> {
351 match hole.kind {
352 FrameHoleKind::Ref | FrameHoleKind::Term | FrameHoleKind::Choice => match value {
353 Expr::Symbol(_) => Ok(()),
354 Expr::String(text) if is_token(text) => Ok(()),
355 Expr::String(_) => Err(Error::Eval(format!(
356 "BRIDGE prose is only allowed in Prose hole {}",
357 hole.name
358 ))),
359 _ => Err(Error::Eval(format!(
360 "BRIDGE hole {} expects a symbol or token",
361 hole.name
362 ))),
363 },
364 FrameHoleKind::Path => match value {
365 Expr::Vector(items) | Expr::List(items) if !items.is_empty() => {
366 for item in items {
367 match item {
368 Expr::Symbol(_) => {}
369 Expr::String(text) if is_token(text) => {}
370 _ => {
371 return Err(Error::Eval(format!(
372 "BRIDGE path hole {} expects token path items",
373 hole.name
374 )));
375 }
376 }
377 }
378 Ok(())
379 }
380 _ => Err(Error::Eval(format!(
381 "BRIDGE hole {} expects a non-empty path",
382 hole.name
383 ))),
384 },
385 FrameHoleKind::Number => match value {
386 Expr::Number(_) => Ok(()),
387 _ => Err(Error::Eval(format!(
388 "BRIDGE hole {} expects a number",
389 hole.name
390 ))),
391 },
392 FrameHoleKind::Prose => match value {
393 Expr::String(_) => Ok(()),
394 _ => Err(Error::Eval(format!(
395 "BRIDGE hole {} expects prose text",
396 hole.name
397 ))),
398 },
399 }
400}
401
402fn frame(
403 name: &str,
404 kind: FrameKind,
405 template: &'static str,
406 holes: &[(&str, FrameHoleKind)],
407) -> FrameSpec {
408 FrameSpec::new(
409 Symbol::qualified("bridge", name),
410 kind,
411 template,
412 holes
413 .iter()
414 .map(|(name, kind)| FrameHoleSpec::new(Symbol::new(*name), *kind))
415 .collect(),
416 )
417}
418
419fn map_fields<'a>(expr: &'a Expr, label: &str) -> Result<&'a [(Expr, Expr)]> {
420 match expr {
421 Expr::Map(fields) => Ok(fields),
422 _ => Err(Error::Eval(format!("{label} must be a map"))),
423 }
424}
425
426fn reject_unknown(fields: &[(Expr, Expr)], allowed: &[&str]) -> Result<()> {
427 for (key, _) in fields {
428 let Some(symbol) = key_symbol(key) else {
429 return Err(Error::Eval(
430 "BRIDGE frame field keys must be symbols".to_owned(),
431 ));
432 };
433 if !allowed.contains(&symbol.name.as_ref()) {
434 return Err(Error::Eval(format!("unknown BRIDGE frame field {symbol}")));
435 }
436 }
437 Ok(())
438}
439
440fn required_symbol(fields: &[(Expr, Expr)], name: &str) -> Result<Symbol> {
441 match field_value(fields, name) {
442 Some(Expr::Symbol(symbol)) => Ok(symbol.clone()),
443 Some(_) => Err(Error::Eval(format!(
444 "BRIDGE frame field {name} must be a symbol"
445 ))),
446 None => Err(Error::Eval(format!("BRIDGE frame payload missing {name}"))),
447 }
448}
449
450fn optional_slots(fields: &[(Expr, Expr)], name: &str) -> Result<BTreeMap<Symbol, Expr>> {
451 let Some(value) = field_value(fields, name) else {
452 return Ok(BTreeMap::new());
453 };
454 let Expr::Map(entries) = value else {
455 return Err(Error::Eval("BRIDGE frame slots must be a map".to_owned()));
456 };
457 let mut slots = BTreeMap::new();
458 for (key, value) in entries {
459 let Some(symbol) = key_symbol(key) else {
460 return Err(Error::Eval(
461 "BRIDGE frame slot keys must be symbols".to_owned(),
462 ));
463 };
464 slots.insert(symbol.clone(), value.clone());
465 }
466 Ok(slots)
467}
468
469fn field_value<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Option<&'a Expr> {
470 fields.iter().find_map(|(key, value)| {
471 let symbol = key_symbol(key)?;
472 (symbol.name.as_ref() == name).then_some(value)
473 })
474}
475
476fn key_symbol(key: &Expr) -> Option<&Symbol> {
477 match key {
478 Expr::Symbol(symbol) => Some(symbol),
479 _ => None,
480 }
481}
482
483fn is_token(text: &str) -> bool {
484 !text.is_empty()
485 && text
486 .chars()
487 .all(|ch| ch.is_ascii() && !ch.is_whitespace() && !matches!(ch, '{' | '}'))
488}