Skip to main content

sim_codec_bridge/
weave.rs

1use std::collections::BTreeSet;
2
3use sim_kernel::{Error, Expr, Result, Symbol};
4use sim_value::build::entry;
5
6/// One operation-led LOOM row.
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct BridgeWeaveRow {
9    /// Slot this row defines for later rows.
10    pub slot: String,
11    /// Operation or frontier head selected by this row.
12    pub head: Symbol,
13    /// Role bindings carried by this row.
14    pub roles: Vec<(Symbol, Expr)>,
15}
16
17impl BridgeWeaveRow {
18    /// Builds a weave row.
19    pub fn new(slot: impl Into<String>, head: Symbol, roles: Vec<(Symbol, Expr)>) -> Self {
20        Self {
21            slot: slot.into(),
22            head,
23            roles,
24        }
25    }
26
27    /// Decodes a weave row expression.
28    pub fn from_expr(expr: &Expr) -> Result<Self> {
29        let fields = map_fields(expr, "bridge/WeaveRow")?;
30        reject_unknown(fields, &["slot", "head", "roles"])?;
31        Ok(Self::new(
32            required_string(fields, "slot")?.to_owned(),
33            required_symbol(fields, "head")?.clone(),
34            required_roles(fields, "roles")?,
35        ))
36    }
37
38    /// Encodes this row as a canonical expression map.
39    pub fn to_expr(&self) -> Expr {
40        Expr::Map(vec![
41            entry("slot", Expr::String(self.slot.clone())),
42            entry("head", Expr::Symbol(self.head.clone())),
43            entry(
44                "roles",
45                Expr::Map(
46                    self.roles
47                        .iter()
48                        .map(|(role, value)| (Expr::Symbol(role.clone()), value.clone()))
49                        .collect(),
50                ),
51            ),
52        ])
53    }
54}
55
56/// Typed payload for a `bridge/Weave` part.
57#[derive(Clone, Debug, PartialEq, Eq)]
58pub struct BridgeWeavePayload {
59    /// Ordered LOOM rows.
60    pub rows: Vec<BridgeWeaveRow>,
61    /// Checker-owned result Shape derived from `rows`.
62    pub result_shape: Expr,
63}
64
65impl BridgeWeavePayload {
66    /// Builds a weave payload, deriving the result Shape from its rows.
67    pub fn new(rows: Vec<BridgeWeaveRow>) -> Self {
68        let result_shape = derive_weave_result_shape(&rows);
69        Self { rows, result_shape }
70    }
71
72    /// Decodes a weave payload expression and rejects any handwritten result
73    /// Shape that disagrees with the rows.
74    pub fn from_expr(expr: &Expr) -> Result<Self> {
75        let fields = map_fields(expr, "bridge/Weave payload")?;
76        reject_unknown(fields, &["rows", "result-shape"])?;
77        let rows = required_vector(fields, "rows")?
78            .iter()
79            .map(BridgeWeaveRow::from_expr)
80            .collect::<Result<Vec<_>>>()?;
81        let result_shape = required_field(fields, "result-shape")?.clone();
82        let derived = derive_weave_result_shape(&rows);
83        if result_shape != derived {
84            return Err(Error::Eval(
85                "BRIDGE weave result-shape disagrees with derived row Shape".to_owned(),
86            ));
87        }
88        Ok(Self { rows, result_shape })
89    }
90
91    /// Encodes this payload as a canonical expression map.
92    pub fn to_expr(&self) -> Expr {
93        Expr::Map(vec![
94            entry(
95                "rows",
96                Expr::Vector(self.rows.iter().map(BridgeWeaveRow::to_expr).collect()),
97            ),
98            entry("result-shape", derive_weave_result_shape(&self.rows)),
99        ])
100    }
101}
102
103/// Roadmap-facing alias for a typed weave part payload.
104pub type WeavePart = BridgeWeavePayload;
105
106/// Derives the public result Shape descriptor for a sequence of weave rows.
107pub fn derive_weave_result_shape(rows: &[BridgeWeaveRow]) -> Expr {
108    Expr::Map(vec![
109        entry(
110            "shape",
111            Expr::Symbol(Symbol::qualified("bridge", "WeaveResult")),
112        ),
113        entry(
114            "slots",
115            Expr::Vector(
116                rows.iter()
117                    .map(|row| {
118                        Expr::Map(vec![
119                            entry("slot", Expr::String(row.slot.clone())),
120                            entry("head", Expr::Symbol(row.head.clone())),
121                        ])
122                    })
123                    .collect(),
124            ),
125        ),
126    ])
127}
128
129/// Parses and validates a `bridge/Weave` payload.
130pub fn validate_weave_payload(payload: &Expr) -> Result<BridgeWeavePayload> {
131    let payload = BridgeWeavePayload::from_expr(payload)?;
132    if payload.rows.is_empty() {
133        return Err(Error::Eval(
134            "BRIDGE weave payload must contain at least one row".to_owned(),
135        ));
136    }
137    let mut slots = BTreeSet::new();
138    for row in &payload.rows {
139        if !is_token(&row.slot) {
140            return Err(Error::Eval(format!(
141                "BRIDGE weave slot {} must be a token",
142                row.slot
143            )));
144        }
145        if !slots.insert(row.slot.clone()) {
146            return Err(Error::Eval(format!(
147                "BRIDGE weave slot {} is duplicated",
148                row.slot
149            )));
150        }
151    }
152    Ok(payload)
153}
154
155fn map_fields<'a>(expr: &'a Expr, label: &str) -> Result<&'a [(Expr, Expr)]> {
156    match expr {
157        Expr::Map(fields) => Ok(fields),
158        _ => Err(Error::Eval(format!("{label} must be a map"))),
159    }
160}
161
162fn reject_unknown(fields: &[(Expr, Expr)], allowed: &[&str]) -> Result<()> {
163    for (key, _) in fields {
164        let Some(name) = field_name(key) else {
165            return Err(Error::Eval(
166                "BRIDGE weave field keys must be symbols".to_owned(),
167            ));
168        };
169        if !allowed.contains(&name.as_str()) {
170            return Err(Error::Eval(format!("unknown BRIDGE weave field {name}")));
171        }
172    }
173    Ok(())
174}
175
176fn required_field<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Result<&'a Expr> {
177    fields
178        .iter()
179        .find_map(|(key, value)| (field_name(key).as_deref() == Some(name)).then_some(value))
180        .ok_or_else(|| Error::Eval(format!("BRIDGE weave record is missing {name}")))
181}
182
183fn required_symbol<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Result<&'a Symbol> {
184    match required_field(fields, name)? {
185        Expr::Symbol(symbol) => Ok(symbol),
186        _ => Err(Error::TypeMismatch {
187            expected: "symbol",
188            found: "non-symbol",
189        }),
190    }
191}
192
193fn required_string<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Result<&'a str> {
194    match required_field(fields, name)? {
195        Expr::String(value) => Ok(value),
196        _ => Err(Error::TypeMismatch {
197            expected: "string",
198            found: "non-string",
199        }),
200    }
201}
202
203fn required_vector<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Result<&'a [Expr]> {
204    match required_field(fields, name)? {
205        Expr::Vector(items) | Expr::List(items) => Ok(items),
206        _ => Err(Error::Eval(format!(
207            "BRIDGE weave {name} field must be a vector"
208        ))),
209    }
210}
211
212fn required_roles(fields: &[(Expr, Expr)], name: &str) -> Result<Vec<(Symbol, Expr)>> {
213    let Expr::Map(entries) = required_field(fields, name)? else {
214        return Err(Error::Eval(format!(
215            "BRIDGE weave {name} field must be a map"
216        )));
217    };
218    entries
219        .iter()
220        .map(|(key, value)| match key {
221            Expr::Symbol(symbol) => Ok((symbol.clone(), value.clone())),
222            _ => Err(Error::Eval(
223                "BRIDGE weave role keys must be symbols".to_owned(),
224            )),
225        })
226        .collect()
227}
228
229fn field_name(expr: &Expr) -> Option<String> {
230    match expr {
231        Expr::Symbol(symbol) => Some(symbol.name.to_string()),
232        Expr::String(value) => Some(value.clone()),
233        _ => None,
234    }
235}
236
237fn is_token(text: &str) -> bool {
238    !text.is_empty()
239        && text
240            .chars()
241            .all(|ch| ch.is_ascii() && !ch.is_whitespace() && !matches!(ch, '{' | '}'))
242}