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#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct ContractCard {
11 pub lib: Symbol,
13 pub export_kind: Symbol,
15 pub symbol: Symbol,
17 pub args_shape: Option<Expr>,
19 pub result_shape: Option<Expr>,
21 pub capability_symbols: Vec<Symbol>,
23 pub card_requires: Option<Expr>,
25 pub summary: String,
27 pub example: Option<Expr>,
29 pub partial: Vec<ContractGap>,
31}
32
33#[derive(Clone, Debug, PartialEq, Eq)]
35pub enum ContractGap {
36 MissingCallableShape,
38 MissingCard,
40 MissingExample,
42 SynthesizedExample,
44}
45
46#[derive(Clone, Debug, Default, PartialEq, Eq)]
48pub struct ContractDeck {
49 pub cards: Vec<ContractCard>,
51 pub diagnostics: Vec<Diagnostic>,
53}
54
55pub 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}