Skip to main content

sim_codec_bridge/
frame_render.rs

1use sim_kernel::{Error, Expr, Result, Symbol};
2
3use crate::{
4    BridgeFramePayload, FrameHoleKind, FrameHoleSpec, FrameSpec, frame_book::validate_frame_payload,
5};
6
7/// Renders a frame payload through its deterministic sentence template.
8///
9/// Prose holes are rejected here because they require a caller-provided fence.
10pub fn render_frame(spec: &FrameSpec, payload: &Expr) -> Result<String> {
11    render_frame_with_prose(spec, payload, |name, _| {
12        Err(Error::Eval(format!(
13            "BRIDGE prose hole {} requires fenced rendering",
14            name.as_qualified_str()
15        )))
16    })
17}
18
19/// Renders a frame payload, delegating prose holes to `render_prose`.
20pub fn render_frame_with_prose<F>(
21    spec: &FrameSpec,
22    payload: &Expr,
23    mut render_prose: F,
24) -> Result<String>
25where
26    F: FnMut(&Symbol, &Expr) -> Result<String>,
27{
28    let payload = BridgeFramePayload::from_expr(payload)?;
29    if payload.frame != spec.id {
30        return Err(Error::Eval(format!(
31            "BRIDGE frame payload {} does not match spec {}",
32            payload.frame, spec.id
33        )));
34    }
35    validate_frame_payload(spec, &payload)?;
36    let mut body = spec.template.to_owned();
37    for hole in &spec.holes {
38        let value = payload.slots.get(&hole.name).ok_or_else(|| {
39            Error::Eval(format!(
40                "BRIDGE frame {} missing hole {}",
41                spec.id, hole.name
42            ))
43        })?;
44        let rendered = render_hole(hole, value, &mut render_prose)?;
45        let marker = format!("{{{}}}", hole.name.name);
46        if !body.contains(&marker) {
47            return Err(Error::Eval(format!(
48                "BRIDGE frame {} template is missing marker {marker}",
49                spec.id
50            )));
51        }
52        body = body.replace(&marker, &rendered);
53    }
54    Ok(format!("{}{}", spec.prefix, body))
55}
56
57fn render_hole<F>(hole: &FrameHoleSpec, value: &Expr, render_prose: &mut F) -> Result<String>
58where
59    F: FnMut(&Symbol, &Expr) -> Result<String>,
60{
61    match hole.kind {
62        FrameHoleKind::Ref | FrameHoleKind::Term | FrameHoleKind::Choice => token_value(value),
63        FrameHoleKind::Path => path_value(value),
64        FrameHoleKind::Number => match value {
65            Expr::Number(number) => Ok(number.canonical.clone()),
66            _ => unreachable!("validated number hole"),
67        },
68        FrameHoleKind::Prose => render_prose(&hole.name, value),
69    }
70}
71
72fn token_value(value: &Expr) -> Result<String> {
73    match value {
74        Expr::Symbol(symbol) => Ok(symbol.as_qualified_str().to_owned()),
75        Expr::String(text) => Ok(text.clone()),
76        _ => unreachable!("validated token hole"),
77    }
78}
79
80fn path_value(value: &Expr) -> Result<String> {
81    let items = match value {
82        Expr::Vector(items) | Expr::List(items) => items,
83        _ => unreachable!("validated path hole"),
84    };
85    items
86        .iter()
87        .map(token_value)
88        .collect::<Result<Vec<_>>>()
89        .map(|parts| parts.join("."))
90}