Skip to main content

sim_lib_forge/
contract_expr.rs

1use sim_kernel::{Cx, Error, Expr, Result, ShapeId, Symbol};
2use sim_shape::{check_shape_on_expr, parse_shape_expr};
3use sim_value::{
4    access::{entry_field, map_entries},
5    build::entry,
6};
7
8use crate::{ContractCard, ContractGap};
9
10impl ContractCard {
11    /// Encodes this card as tagged open data accepted by [`contract_card_shape`].
12    pub fn as_expr(&self) -> Expr {
13        Expr::Map(vec![
14            entry("kind", Expr::Symbol(contract_card_symbol())),
15            entry("lib", Expr::Symbol(self.lib.clone())),
16            entry("export-kind", Expr::Symbol(self.export_kind.clone())),
17            entry("symbol", Expr::Symbol(self.symbol.clone())),
18            entry("args-shape", option_expr(self.args_shape.clone())),
19            entry("result-shape", option_expr(self.result_shape.clone())),
20            entry(
21                "capabilities",
22                Expr::List(
23                    self.capability_symbols
24                        .iter()
25                        .cloned()
26                        .map(Expr::Symbol)
27                        .collect(),
28                ),
29            ),
30            entry("card-requires", option_expr(self.card_requires.clone())),
31            entry("summary", Expr::String(self.summary.clone())),
32            entry("example", option_expr(self.example.clone())),
33            entry(
34                "partial",
35                Expr::List(
36                    self.partial
37                        .iter()
38                        .map(|gap| Expr::Symbol(gap.as_symbol()))
39                        .collect(),
40                ),
41            ),
42        ])
43    }
44}
45
46impl ContractGap {
47    /// Returns the stable symbol used in encoded contract data.
48    pub fn as_symbol(&self) -> Symbol {
49        match self {
50            Self::MissingCallableShape => {
51                Symbol::qualified("contract-gap", "missing-callable-shape")
52            }
53            Self::MissingCard => Symbol::qualified("contract-gap", "missing-card"),
54            Self::MissingExample => Symbol::qualified("contract-gap", "missing-example"),
55            Self::SynthesizedExample => Symbol::qualified("contract-gap", "synthesized-example"),
56        }
57    }
58
59    fn from_symbol(symbol: &Symbol) -> Result<Self> {
60        let namespace = symbol.namespace.as_deref();
61        let name = symbol.name.as_ref();
62        match (namespace, name) {
63            (Some("contract-gap"), "missing-callable-shape") => Ok(Self::MissingCallableShape),
64            (Some("contract-gap"), "missing-card") => Ok(Self::MissingCard),
65            (Some("contract-gap"), "missing-example") => Ok(Self::MissingExample),
66            (Some("contract-gap"), "synthesized-example") => Ok(Self::SynthesizedExample),
67            _ => Err(Error::Eval(format!("unknown contract gap {symbol}"))),
68        }
69    }
70}
71
72/// The Shape a `ContractCard::as_expr` value conforms to.
73///
74/// Other repos can check tagged deck data against this Shape instead of trusting
75/// untyped `Expr` payloads.
76pub fn contract_card_shape() -> Expr {
77    Expr::List(vec![
78        Expr::Symbol(Symbol::qualified("shape", "table-open")),
79        Expr::List(vec![
80            shape_field("kind", symbol_shape()),
81            shape_field("lib", symbol_shape()),
82            shape_field("export-kind", symbol_shape()),
83            shape_field("symbol", symbol_shape()),
84            shape_field("args-shape", any_shape()),
85            shape_field("result-shape", any_shape()),
86            shape_field(
87                "capabilities",
88                Expr::List(vec![
89                    Expr::Symbol(Symbol::qualified("shape", "repeat")),
90                    symbol_shape(),
91                ]),
92            ),
93            shape_field("card-requires", any_shape()),
94            shape_field("summary", string_shape()),
95            shape_field("example", any_shape()),
96            shape_field(
97                "partial",
98                Expr::List(vec![
99                    Expr::Symbol(Symbol::qualified("shape", "repeat")),
100                    symbol_shape(),
101                ]),
102            ),
103        ]),
104    ])
105}
106
107/// Decodes a [`ContractCard`] from data that checks against [`contract_card_shape`].
108pub fn contract_card_from_expr(cx: &mut Cx, e: &Expr) -> Result<ContractCard> {
109    let shape = parse_shape_expr(&contract_card_shape())?;
110    let checked = check_shape_on_expr(shape.as_ref(), cx, e)?;
111    if !checked.accepted {
112        return Err(Error::WrongShape {
113            expected: ShapeId(0),
114            diagnostics: checked.diagnostics,
115        });
116    }
117
118    let entries = map_entries(e, "contract card")?;
119    let kind = required_symbol(entries, "kind")?;
120    if kind != contract_card_symbol() {
121        return Err(Error::Eval(format!(
122            "contract card kind must be {}, found {kind}",
123            contract_card_symbol()
124        )));
125    }
126
127    Ok(ContractCard {
128        lib: required_symbol(entries, "lib")?,
129        export_kind: required_symbol(entries, "export-kind")?,
130        symbol: required_symbol(entries, "symbol")?,
131        args_shape: optional_expr(entries, "args-shape")?,
132        result_shape: optional_expr(entries, "result-shape")?,
133        capability_symbols: required_symbol_list(entries, "capabilities")?,
134        card_requires: optional_expr(entries, "card-requires")?,
135        summary: required_string(entries, "summary")?,
136        example: optional_expr(entries, "example")?,
137        partial: required_symbol_list(entries, "partial")?
138            .iter()
139            .map(ContractGap::from_symbol)
140            .collect::<Result<Vec<_>>>()?,
141    })
142}
143
144fn option_expr(expr: Option<Expr>) -> Expr {
145    expr.unwrap_or(Expr::Nil)
146}
147
148fn optional_expr(entries: &[(Expr, Expr)], field: &str) -> Result<Option<Expr>> {
149    Ok(match required_expr(entries, field)? {
150        Expr::Nil => None,
151        expr => Some(expr.clone()),
152    })
153}
154
155fn required_expr<'a>(entries: &'a [(Expr, Expr)], field: &str) -> Result<&'a Expr> {
156    entry_field(entries, field)
157        .ok_or_else(|| Error::Eval(format!("contract card is missing field {field}")))
158}
159
160fn required_symbol(entries: &[(Expr, Expr)], field: &str) -> Result<Symbol> {
161    match required_expr(entries, field)? {
162        Expr::Symbol(symbol) => Ok(symbol.clone()),
163        _ => Err(Error::Eval(format!(
164            "contract card field {field} must be a symbol"
165        ))),
166    }
167}
168
169fn required_string(entries: &[(Expr, Expr)], field: &str) -> Result<String> {
170    match required_expr(entries, field)? {
171        Expr::String(text) => Ok(text.clone()),
172        _ => Err(Error::Eval(format!(
173            "contract card field {field} must be a string"
174        ))),
175    }
176}
177
178fn required_symbol_list(entries: &[(Expr, Expr)], field: &str) -> Result<Vec<Symbol>> {
179    match required_expr(entries, field)? {
180        Expr::List(items) => items
181            .iter()
182            .map(|item| match item {
183                Expr::Symbol(symbol) => Ok(symbol.clone()),
184                _ => Err(Error::Eval(format!(
185                    "contract card field {field} must contain only symbols"
186                ))),
187            })
188            .collect(),
189        _ => Err(Error::Eval(format!(
190            "contract card field {field} must be a list"
191        ))),
192    }
193}
194
195fn shape_field(field: &str, shape: Expr) -> Expr {
196    Expr::List(vec![Expr::Symbol(Symbol::new(field)), shape])
197}
198
199fn any_shape() -> Expr {
200    Expr::Symbol(Symbol::new("Any"))
201}
202
203fn string_shape() -> Expr {
204    Expr::Symbol(Symbol::new("String"))
205}
206
207fn symbol_shape() -> Expr {
208    Expr::Symbol(Symbol::new("Symbol"))
209}
210
211fn contract_card_symbol() -> Symbol {
212    Symbol::qualified("forge", "contract-card")
213}