Skip to main content

sim_codec_bridge/
frame_book.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use sim_kernel::{Error, Expr, Result, Symbol};
4use sim_value::build::entry;
5
6/// Illocutionary kind for a BRIDGE frame.
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum FrameKind {
9    /// A frame that tells the receiver which context to use.
10    Use,
11    /// A frame that supplies information.
12    Inform,
13    /// A frame that assigns work.
14    Task,
15    /// A frame that declares a hard requirement.
16    Require,
17    /// A frame that declares forbidden behavior.
18    Forbid,
19    /// A frame that declares a preference.
20    Prefer,
21    /// A frame that describes return obligations.
22    Return,
23    /// A frame that asks for a check.
24    Check,
25}
26
27impl FrameKind {
28    /// Deterministic sentence prefix for this frame kind.
29    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    /// Grammar priority used when frames become constrained-generation choices.
43    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/// Hole kind for a typed BRIDGE frame slot.
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub enum FrameHoleKind {
57    /// A reference to another packet part or external object.
58    Ref,
59    /// A symbolic term.
60    Term,
61    /// A closed choice token.
62    Choice,
63    /// A structured path.
64    Path,
65    /// A numeric value.
66    Number,
67    /// Prose data that must be rendered through a fence.
68    Prose,
69}
70
71impl FrameHoleKind {
72    /// Shape descriptor for values accepted by this hole kind.
73    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/// One named typed hole in a frame template.
100#[derive(Clone, Debug, PartialEq, Eq)]
101pub struct FrameHoleSpec {
102    /// Hole name used in the payload and template.
103    pub name: Symbol,
104    /// Hole kind.
105    pub kind: FrameHoleKind,
106}
107
108impl FrameHoleSpec {
109    /// Builds a hole spec.
110    pub fn new(name: Symbol, kind: FrameHoleKind) -> Self {
111        Self { name, kind }
112    }
113}
114
115/// Registered frame specification.
116#[derive(Clone, Debug, PartialEq, Eq)]
117pub struct FrameSpec {
118    /// Frame id carried in `bridge/Frame` payloads.
119    pub id: Symbol,
120    /// Illocutionary frame kind.
121    pub kind: FrameKind,
122    /// Deterministic sentence prefix.
123    pub prefix: &'static str,
124    /// Deterministic template body using `{hole}` placeholders.
125    pub template: &'static str,
126    /// Typed holes accepted by this frame.
127    pub holes: Vec<FrameHoleSpec>,
128    /// Priority for grammar menus.
129    pub grammar_priority: u8,
130}
131
132impl FrameSpec {
133    /// Builds a frame spec, deriving prefix and priority from `kind`.
134    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/// Typed payload for a `bridge/Frame` part.
152#[derive(Clone, Debug, PartialEq, Eq)]
153pub struct BridgeFramePayload {
154    /// Registered frame id.
155    pub frame: Symbol,
156    /// Slot values keyed by hole name.
157    pub slots: BTreeMap<Symbol, Expr>,
158}
159
160impl BridgeFramePayload {
161    /// Builds a payload for `frame` with no slots.
162    pub fn new(frame: Symbol) -> Self {
163        Self {
164            frame,
165            slots: BTreeMap::new(),
166        }
167    }
168
169    /// Adds one slot value.
170    pub fn with_slot(mut self, name: Symbol, value: Expr) -> Self {
171        self.slots.insert(name, value);
172        self
173    }
174
175    /// Decodes a payload expression.
176    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    /// Encodes this payload as a canonical expression map.
186    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/// Registry of BRIDGE frame specifications.
204#[derive(Clone, Debug, Default, PartialEq, Eq)]
205pub struct BridgeFrameBook {
206    specs: BTreeMap<Symbol, FrameSpec>,
207}
208
209impl BridgeFrameBook {
210    /// Builds an empty frame book.
211    pub fn new() -> Self {
212        Self::default()
213    }
214
215    /// Registers a frame spec.
216    pub fn register(&mut self, spec: FrameSpec) {
217        self.specs.insert(spec.id.clone(), spec);
218    }
219
220    /// Returns the registered spec for `id`.
221    pub fn spec(&self, id: &Symbol) -> Option<&FrameSpec> {
222        self.specs.get(id)
223    }
224
225    /// Requires a registered frame spec.
226    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    /// Returns all registered specs.
232    pub fn specs(&self) -> impl Iterator<Item = &FrameSpec> {
233        self.specs.values()
234    }
235
236    /// Parses and validates a frame payload.
237    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
245/// Builds the standard BRIDGE frame book.
246pub 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}