Skip to main content

sim_lib_forge/
contracts.rs

1use sim_kernel::{
2    Cx, Diagnostic, Expr, NumberLiteral, Ref, Result, RuntimeId, Symbol, Value,
3    card::card_for_ref,
4    library::{ExportKind, ExportRecord, ExportState, LoadedLib},
5};
6use sim_value::access::entry_field;
7
8/// A compact, source-free model-facing contract for one loaded export.
9#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct ContractCard {
11    /// Library that contributed the export.
12    pub lib: Symbol,
13    /// Export kind, carried as the kernel's open export-kind symbol.
14    pub export_kind: Symbol,
15    /// Export symbol.
16    pub symbol: Symbol,
17    /// Callable argument Shape encoded as data when known.
18    pub args_shape: Option<Expr>,
19    /// Callable result Shape encoded as data when known.
20    pub result_shape: Option<Expr>,
21    /// Library manifest capability requests, as `capability/*` symbols.
22    pub capability_symbols: Vec<Symbol>,
23    /// Browse Card `requires` data, when present.
24    pub card_requires: Option<Expr>,
25    /// Human-facing Card summary text.
26    pub summary: String,
27    /// Example expression, authored or synthesized from the callable Shape.
28    pub example: Option<Expr>,
29    /// Missing or synthesized pieces retained without dropping the export.
30    pub partial: Vec<ContractGap>,
31}
32
33/// A missing or synthesized part of a [`ContractCard`].
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub enum ContractGap {
36    /// The export is callable-shaped but does not expose both argument and result Shapes.
37    MissingCallableShape,
38    /// The browse Card did not provide summary text.
39    MissingCard,
40    /// No authored example was available and none could be synthesized.
41    MissingExample,
42    /// No authored example was available, so FORGE synthesized one from Shape data.
43    SynthesizedExample,
44}
45
46/// A stable, ordered deck of runtime contract cards and assembly diagnostics.
47#[derive(Clone, Debug, Default, PartialEq, Eq)]
48pub struct ContractDeck {
49    /// Cards assembled from loaded registry exports.
50    pub cards: Vec<ContractCard>,
51    /// Non-fatal diagnostics recorded while preserving partial exports.
52    pub diagnostics: Vec<Diagnostic>,
53}
54
55/// Assembles a source-free contract deck from the live registry and browse Cards.
56pub fn assemble_contract_deck(cx: &mut Cx) -> Result<ContractDeck> {
57    let loaded_libs = cx.registry().libs().to_vec();
58    let mut cards = Vec::new();
59    let mut diagnostics = Vec::new();
60
61    for loaded in loaded_libs {
62        let capability_symbols = loaded
63            .manifest
64            .capabilities
65            .iter()
66            .map(|capability| capability.as_symbol())
67            .collect::<Vec<_>>();
68
69        for export in &loaded.exports {
70            let runtime_value = export_value(cx, export);
71            let callable = runtime_value
72                .as_ref()
73                .and_then(|value| value.object().as_callable());
74            let mut partial = Vec::new();
75
76            let (args_shape_ref, args_shape, result_shape) = if let Some(callable) = callable {
77                let args_shape_ref = callable.browse_args_shape(cx)?;
78                let result_shape_ref = callable.browse_result_shape(cx)?;
79                let args_shape = args_shape_ref
80                    .as_ref()
81                    .map(|shape| shape.object().as_expr(cx))
82                    .transpose()?;
83                let result_shape = result_shape_ref
84                    .as_ref()
85                    .map(|shape| shape.object().as_expr(cx))
86                    .transpose()?;
87                if args_shape.is_none() || result_shape.is_none() {
88                    partial.push(ContractGap::MissingCallableShape);
89                }
90                (args_shape_ref, args_shape, result_shape)
91            } else {
92                if export.kind.name() == Some(ExportKind::FUNCTION) {
93                    partial.push(ContractGap::MissingCallableShape);
94                }
95                (None, None, None)
96            };
97
98            let card_fields = browse_card_fields(cx, export.symbol.clone())?;
99            let summary = summary_from_card(&card_fields);
100            if summary.is_empty() {
101                partial.push(ContractGap::MissingCard);
102            }
103            let card_requires = card_requires_from_card(&card_fields);
104            let example = match example_from_card(&card_fields) {
105                Some(example) => Some(example),
106                None if callable.is_some() => {
107                    partial.push(ContractGap::SynthesizedExample);
108                    Some(synthesize_example(
109                        cx,
110                        &export.symbol,
111                        args_shape_ref.as_ref(),
112                        args_shape.as_ref(),
113                    )?)
114                }
115                None => {
116                    partial.push(ContractGap::MissingExample);
117                    None
118                }
119            };
120
121            record_partial_diagnostics(&mut diagnostics, &loaded, export, &partial);
122            cards.push(ContractCard {
123                lib: loaded.manifest.id.clone(),
124                export_kind: export.kind.symbol().clone(),
125                symbol: export.symbol.clone(),
126                args_shape,
127                result_shape,
128                capability_symbols: capability_symbols.clone(),
129                card_requires,
130                summary,
131                example,
132                partial,
133            });
134        }
135    }
136
137    cards.sort_by(|left, right| {
138        (&left.lib, &left.export_kind, &left.symbol).cmp(&(
139            &right.lib,
140            &right.export_kind,
141            &right.symbol,
142        ))
143    });
144    Ok(ContractDeck { cards, diagnostics })
145}
146
147pub(crate) fn export_value(cx: &Cx, export: &ExportRecord) -> Option<Value> {
148    let id = cx
149        .registry()
150        .export_symbols()
151        .get(&export.kind)?
152        .get(&export.symbol)?;
153    match id {
154        RuntimeId::Class(id) => cx.registry().class_value(*id).cloned(),
155        RuntimeId::Function(id) => cx.registry().function_value(*id).cloned(),
156        RuntimeId::Macro(id) => cx.registry().macro_value(*id).cloned(),
157        RuntimeId::Shape(id) => cx.registry().shape_value(*id).cloned(),
158        RuntimeId::Codec(id) => cx.registry().codec_value(*id).cloned(),
159        RuntimeId::NumberDomain(id) => cx.registry().number_domain_value(*id).cloned(),
160        RuntimeId::Site(_) => cx.registry().site_value(*id).cloned(),
161        RuntimeId::Value => cx.registry().value_by_symbol(&export.symbol).cloned(),
162    }
163}
164
165fn browse_card_fields(cx: &mut Cx, symbol: Symbol) -> Result<Vec<(Expr, Expr)>> {
166    let card = card_for_ref(cx, Ref::Symbol(symbol))?;
167    match card.object().as_expr(cx)? {
168        Expr::Map(entries) => Ok(entries),
169        _ => Ok(Vec::new()),
170    }
171}
172
173fn summary_from_card(entries: &[(Expr, Expr)]) -> String {
174    let summary = entry_field(entries, "summary").or_else(|| entry_field(entries, "help"));
175    match summary {
176        Some(Expr::String(summary)) => summary.trim().to_owned(),
177        _ => String::new(),
178    }
179}
180
181fn card_requires_from_card(entries: &[(Expr, Expr)]) -> Option<Expr> {
182    match entry_field(entries, "requires") {
183        Some(Expr::List(items)) if !items.is_empty() => Some(Expr::List(items.clone())),
184        Some(expr) if !matches!(expr, Expr::Nil) => Some(expr.clone()),
185        _ => None,
186    }
187}
188
189fn example_from_card(entries: &[(Expr, Expr)]) -> Option<Expr> {
190    ["example", "expr"]
191        .iter()
192        .find_map(|field| match entry_field(entries, field) {
193            Some(expr) if !matches!(expr, Expr::Nil) => Some(expr.clone()),
194            _ => None,
195        })
196}
197
198fn synthesize_example(
199    cx: &mut Cx,
200    symbol: &Symbol,
201    args_shape_ref: Option<&Value>,
202    args_shape: Option<&Expr>,
203) -> Result<Expr> {
204    Ok(Expr::Call {
205        operator: Box::new(Expr::Symbol(symbol.clone())),
206        args: synthesize_args(cx, args_shape_ref, args_shape)?,
207    })
208}
209
210fn synthesize_args(
211    cx: &mut Cx,
212    args_shape_ref: Option<&Value>,
213    args_shape: Option<&Expr>,
214) -> Result<Vec<Expr>> {
215    let mut candidates = Vec::new();
216    if let Some(args_shape) = args_shape {
217        candidates.push(args_from_shape_expr(args_shape));
218    }
219    candidates.extend([
220        Vec::new(),
221        vec![Expr::String("example".to_owned())],
222        vec![Expr::Bool(true)],
223        vec![Expr::Number(NumberLiteral {
224            domain: Symbol::qualified("core", "Number"),
225            canonical: "0".to_owned(),
226        })],
227        vec![Expr::Symbol(Symbol::new("example"))],
228        vec![Expr::Nil],
229    ]);
230
231    let Some(args_shape_ref) = args_shape_ref else {
232        return Ok(candidates.into_iter().next().unwrap_or_default());
233    };
234    let Some(shape) = args_shape_ref.object().as_shape() else {
235        return Ok(candidates.into_iter().next().unwrap_or_default());
236    };
237    for args in candidates {
238        let expr = Expr::List(args.clone());
239        if shape.check_expr(cx, &expr)?.accepted {
240            return Ok(args);
241        }
242    }
243    Ok(Vec::new())
244}
245
246fn args_from_shape_expr(shape: &Expr) -> Vec<Expr> {
247    let Expr::List(items) = shape else {
248        return Vec::new();
249    };
250    let Some(Expr::Symbol(head)) = items.first() else {
251        return Vec::new();
252    };
253    if shape_name(head) != Some("list") {
254        return Vec::new();
255    }
256    let shape_items = if head.namespace.as_deref() == Some("shape") {
257        match items.get(1) {
258            Some(Expr::List(items)) => items.as_slice(),
259            _ => &[],
260        }
261    } else {
262        &items[1..]
263    };
264    shape_items.iter().map(default_expr_for_shape).collect()
265}
266
267fn default_expr_for_shape(shape: &Expr) -> Expr {
268    match shape {
269        Expr::Symbol(symbol) if symbol.namespace.is_none() => match symbol.name.as_ref() {
270            "String" => Expr::String("example".to_owned()),
271            "Bool" => Expr::Bool(true),
272            "Number" => Expr::Number(NumberLiteral {
273                domain: Symbol::qualified("core", "Number"),
274                canonical: "0".to_owned(),
275            }),
276            "Symbol" => Expr::Symbol(Symbol::new("example")),
277            "List" => Expr::List(Vec::new()),
278            "Map" => Expr::Map(Vec::new()),
279            _ => Expr::Nil,
280        },
281        Expr::Symbol(symbol)
282            if symbol.namespace.as_deref() == Some("core") && symbol.name.as_ref() == "Number" =>
283        {
284            Expr::Number(NumberLiteral {
285                domain: Symbol::qualified("core", "Number"),
286                canonical: "0".to_owned(),
287            })
288        }
289        _ => Expr::Nil,
290    }
291}
292
293fn record_partial_diagnostics(
294    diagnostics: &mut Vec<Diagnostic>,
295    loaded: &LoadedLib,
296    export: &ExportRecord,
297    partial: &[ContractGap],
298) {
299    for gap in partial {
300        diagnostics.push(Diagnostic::info(format!(
301            "{} {} from {} has {}",
302            export.kind.symbol(),
303            export.symbol,
304            loaded.manifest.id,
305            gap.as_symbol()
306        )));
307    }
308    match &export.state {
309        ExportState::Resolved { .. } | ExportState::Declared => {}
310        ExportState::Unsupported { reason } => diagnostics.push(Diagnostic::info(format!(
311            "{} {} is unsupported: {reason}",
312            export.kind.symbol(),
313            export.symbol
314        ))),
315        ExportState::Invalid { error } => diagnostics.push(Diagnostic::error(format!(
316            "{} {} is invalid: {error}",
317            export.kind.symbol(),
318            export.symbol
319        ))),
320    }
321}
322
323fn shape_name(symbol: &Symbol) -> Option<&str> {
324    if symbol.namespace.is_none() || symbol.namespace.as_deref() == Some("shape") {
325        Some(symbol.name.as_ref())
326    } else {
327        None
328    }
329}