1mod graph;
9
10use sim_kernel::{Error, Expr, Result, Symbol};
11
12use crate::{
13 AnyShape, ExactExprShape, ExprKind, ExprKindShape, FieldShape, ListShape, OneOfShape, Shape,
14 ShapeDefRef, ShapeDefs,
15};
16
17pub use graph::{
18 GrammarDialect, GrammarGraph, GrammarPosition, GrammarTarget, Production, ShapeGrammar,
19 TerminalAtom,
20};
21
22pub fn shape_grammar_graph(shape: &dyn Shape) -> Result<GrammarGraph> {
24 let lowered = lower_shape(shape)?;
25 Ok(GrammarGraph {
26 root: lowered.production,
27 defs: lowered.defs,
28 diagnostics: Vec::new(),
29 })
30}
31
32pub fn shape_json_schema(shape: &dyn Shape) -> Result<String> {
38 let graph = shape_grammar_graph(shape)?;
39 render_json_graph(&graph)
40}
41
42pub trait GrammarRenderer {
44 fn codec_symbol(&self) -> Symbol;
46
47 fn dialect(&self) -> GrammarDialect;
49
50 fn render(&self, graph: &GrammarGraph, position: GrammarPosition) -> Result<String>;
52}
53
54pub fn shape_grammar(
56 shape: &dyn Shape,
57 target: GrammarTarget,
58 renderer: &dyn GrammarRenderer,
59) -> Result<ShapeGrammar> {
60 let renderer_codec = renderer.codec_symbol();
61 if renderer_codec != target.codec {
62 return Err(unsupported_shape(format!(
63 "grammar renderer targets codec {}, not {}",
64 renderer_codec, target.codec
65 )));
66 }
67 let renderer_dialect = renderer.dialect();
68 if renderer_dialect != target.dialect {
69 return Err(unsupported_shape(format!(
70 "grammar renderer targets dialect {:?}, not {:?}",
71 renderer_dialect, target.dialect
72 )));
73 }
74 let graph = shape_grammar_graph(shape)?;
75 let text = renderer.render(&graph, target.position)?;
76 let diagnostics = graph.diagnostics.clone();
77 Ok(ShapeGrammar {
78 target,
79 graph,
80 text,
81 diagnostics,
82 })
83}
84
85struct LoweredProduction {
86 production: Production,
87 defs: Vec<(Symbol, Production)>,
88}
89
90impl LoweredProduction {
91 fn new(production: Production) -> Self {
92 Self {
93 production,
94 defs: Vec::new(),
95 }
96 }
97}
98
99fn lower_shape(shape: &dyn Shape) -> Result<LoweredProduction> {
100 if shape.as_any().is::<AnyShape>() {
101 return Ok(LoweredProduction::new(Production::Alt(vec![
102 Production::Terminal(TerminalAtom::Any),
103 ])));
104 }
105 if let Some(kind) = shape.as_any().downcast_ref::<ExprKindShape>() {
106 return lower_expr_kind(kind.kind());
107 }
108 if let Some(defs) = shape.as_any().downcast_ref::<ShapeDefs>() {
109 let root = lower_shape(defs.root().as_ref())?;
110 let mut graph_defs = root.defs;
111 for (name, shape) in defs.defs() {
112 let lowered = lower_shape(shape.as_ref())?;
113 graph_defs.extend(lowered.defs);
114 graph_defs.push((name.clone(), lowered.production));
115 }
116 return Ok(LoweredProduction {
117 production: root.production,
118 defs: graph_defs,
119 });
120 }
121 if let Some(reference) = shape.as_any().downcast_ref::<ShapeDefRef>() {
122 return Ok(LoweredProduction::new(Production::Ref(
123 reference.name().clone(),
124 )));
125 }
126 if let Some(fields) = shape.as_any().downcast_ref::<FieldShape>() {
127 return lower_field_shape(fields);
128 }
129 if let Some(list) = shape.as_any().downcast_ref::<ListShape>() {
130 return lower_list_shape(list);
131 }
132 if let Some(one_of) = shape.as_any().downcast_ref::<OneOfShape>() {
133 let choices = one_of
134 .choices()
135 .iter()
136 .map(|choice| lower_shape(choice.as_ref()))
137 .collect::<Result<Vec<_>>>()?;
138 let mut defs = Vec::new();
139 let choices = choices
140 .into_iter()
141 .map(|choice| {
142 defs.extend(choice.defs);
143 choice.production
144 })
145 .collect();
146 return Ok(LoweredProduction {
147 production: Production::Alt(choices),
148 defs,
149 });
150 }
151 if let Some(exact) = shape.as_any().downcast_ref::<ExactExprShape>() {
152 return Ok(LoweredProduction::new(Production::Terminal(
153 TerminalAtom::Exact(exact.expected().clone()),
154 )));
155 }
156 Err(unsupported_shape(
157 "shape_grammar_graph does not support this shape",
158 ))
159}
160
161fn lower_expr_kind(kind: &ExprKind) -> Result<LoweredProduction> {
162 let atom = match kind {
163 ExprKind::Nil => TerminalAtom::Nil,
164 ExprKind::Bool => TerminalAtom::Bool,
165 ExprKind::Number => TerminalAtom::Number,
166 ExprKind::String => TerminalAtom::String,
167 ExprKind::List | ExprKind::Vector => TerminalAtom::List,
168 ExprKind::Map => TerminalAtom::Map,
169 ExprKind::Symbol => TerminalAtom::Symbol,
170 other => {
171 return Err(unsupported_shape(format!(
172 "shape_grammar_graph does not support expr-kind {}",
173 other.name()
174 )));
175 }
176 };
177 Ok(LoweredProduction::new(Production::Terminal(atom)))
178}
179
180fn lower_field_shape(shape: &FieldShape) -> Result<LoweredProduction> {
181 let mut defs = Vec::new();
182 let args = shape
183 .fields()
184 .iter()
185 .map(|field| {
186 let lowered = lower_shape(field.shape().as_ref())?;
187 defs.extend(lowered.defs);
188 Ok(Production::Seq(vec![
189 Production::Terminal(TerminalAtom::Exact(Expr::Symbol(field.name().clone()))),
190 lowered.production,
191 ]))
192 })
193 .collect::<Result<Vec<_>>>()?;
194 Ok(LoweredProduction {
195 production: Production::Call {
196 head: Box::new(Production::Terminal(TerminalAtom::Exact(Expr::Symbol(
197 shape
198 .class_symbol()
199 .cloned()
200 .unwrap_or_else(|| Symbol::qualified("shape", "fields")),
201 )))),
202 args,
203 },
204 defs,
205 })
206}
207
208fn lower_list_shape(shape: &ListShape) -> Result<LoweredProduction> {
209 let mut defs = Vec::new();
210 let mut items = shape
211 .items()
212 .iter()
213 .map(|item| {
214 let lowered = lower_shape(item.as_ref())?;
215 defs.extend(lowered.defs);
216 Ok(lowered.production)
217 })
218 .collect::<Result<Vec<_>>>()?;
219 if let Some(rest) = shape.rest() {
220 let lowered = lower_shape(rest.as_ref())?;
221 defs.extend(lowered.defs);
222 items.push(Production::Repeat {
223 inner: Box::new(lowered.production),
224 at_least: 0,
225 });
226 }
227 Ok(LoweredProduction {
228 production: Production::Seq(items),
229 defs,
230 })
231}
232
233fn render_json_graph(graph: &GrammarGraph) -> Result<String> {
234 let root = render_json_schema(&graph.root)?;
235 if graph.defs.is_empty() {
236 return Ok(root);
237 }
238 let defs = graph
239 .defs
240 .iter()
241 .map(|(name, production)| {
242 Ok(format!(
243 "{}:{}",
244 json_string(&name.to_string()),
245 render_json_schema(production)?
246 ))
247 })
248 .collect::<Result<Vec<_>>>()?;
249 Ok(format!(
250 r#"{{"allOf":[{}],"$defs":{{{}}}}}"#,
251 root,
252 defs.join(",")
253 ))
254}
255
256fn render_json_schema(production: &Production) -> Result<String> {
257 match production {
258 Production::Terminal(atom) => render_json_terminal(atom),
259 Production::Seq(items) => render_json_seq(items),
260 Production::Alt(choices) => {
261 if choices.len() == 1
262 && matches!(
263 choices.first(),
264 Some(Production::Terminal(TerminalAtom::Any))
265 )
266 {
267 return Ok("true".to_owned());
268 }
269 let choices = choices
270 .iter()
271 .map(render_json_schema)
272 .collect::<Result<Vec<_>>>()?;
273 Ok(format!(r#"{{"anyOf":[{}]}}"#, choices.join(",")))
274 }
275 Production::Repeat { inner, at_least } => {
276 let min_items = (*at_least > 0).then(|| format!(r#","minItems":{at_least}"#));
277 Ok(format!(
278 r#"{{"type":"array","items":{}{}}}"#,
279 render_json_schema(inner)?,
280 min_items.unwrap_or_default(),
281 ))
282 }
283 Production::Call { head: _, args } => render_json_object(args),
284 Production::Ref(name) => Ok(format!(r##"{{"$ref":"#/$defs/{}"}}"##, name)),
285 }
286}
287
288fn render_json_terminal(atom: &TerminalAtom) -> Result<String> {
289 Ok(match atom {
290 TerminalAtom::Any => "true".to_owned(),
291 TerminalAtom::Nil => r#"{"type":"null"}"#.to_owned(),
292 TerminalAtom::Bool => r#"{"type":"boolean"}"#.to_owned(),
293 TerminalAtom::Number => r#"{"type":"number"}"#.to_owned(),
294 TerminalAtom::String => r#"{"type":"string"}"#.to_owned(),
295 TerminalAtom::List => r#"{"type":"array"}"#.to_owned(),
296 TerminalAtom::Map => r#"{"type":"object"}"#.to_owned(),
297 TerminalAtom::Symbol => r#"{"type":"string","description":"symbol"}"#.to_owned(),
298 TerminalAtom::Exact(expr) => format!(r#"{{"const":{}}}"#, json_expr(expr)?),
299 })
300}
301
302fn render_json_seq(items: &[Production]) -> Result<String> {
303 let (prefix, rest) = match items.split_last() {
304 Some((Production::Repeat { inner, at_least: 0 }, prefix)) => (prefix, Some(inner)),
305 _ => (items, None),
306 };
307 let prefix_items = prefix
308 .iter()
309 .map(render_json_schema)
310 .collect::<Result<Vec<_>>>()?;
311 let items = match rest {
312 Some(rest) => render_json_schema(rest)?,
313 None => "false".to_owned(),
314 };
315 let bounds = rest.is_none().then(|| {
316 format!(
317 r#","minItems":{},"maxItems":{}"#,
318 prefix.len(),
319 prefix.len()
320 )
321 });
322 Ok(format!(
323 r#"{{"type":"array","prefixItems":[{}],"items":{}{}}}"#,
324 prefix_items.join(","),
325 items,
326 bounds.unwrap_or_default(),
327 ))
328}
329
330fn render_json_object(args: &[Production]) -> Result<String> {
331 let mut properties = Vec::new();
332 let mut required = Vec::new();
333 for arg in args {
334 let Production::Seq(parts) = arg else {
335 return Err(unsupported_shape(
336 "shape_json_schema object field must lower to a sequence",
337 ));
338 };
339 let [
340 Production::Terminal(TerminalAtom::Exact(Expr::Symbol(name))),
341 value,
342 ] = parts.as_slice()
343 else {
344 return Err(unsupported_shape(
345 "shape_json_schema object field must start with a symbol name",
346 ));
347 };
348 properties.push(format!(
349 "{}:{}",
350 json_string(name.name.as_ref()),
351 render_json_schema(value)?
352 ));
353 required.push(json_string(name.name.as_ref()));
354 }
355 Ok(format!(
356 r#"{{"type":"object","properties":{{{}}},"required":[{}],"additionalProperties":false}}"#,
357 properties.join(","),
358 required.join(","),
359 ))
360}
361
362fn json_expr(expr: &Expr) -> Result<String> {
363 Ok(match expr {
364 Expr::Nil => "null".to_owned(),
365 Expr::Bool(value) => value.to_string(),
366 Expr::Number(number) => number.canonical.clone(),
367 Expr::String(text) => json_string(text),
368 Expr::Symbol(symbol) => json_string(&symbol.to_string()),
369 Expr::List(items) | Expr::Vector(items) => {
370 let items = items.iter().map(json_expr).collect::<Result<Vec<_>>>()?;
371 format!("[{}]", items.join(","))
372 }
373 Expr::Map(entries) => {
374 let entries = entries
375 .iter()
376 .map(|(key, value)| {
377 let Expr::Symbol(symbol) = key else {
378 return Err(unsupported_shape(
379 "shape_json_schema exact map keys must be symbols",
380 ));
381 };
382 Ok(format!(
383 "{}:{}",
384 json_string(symbol.name.as_ref()),
385 json_expr(value)?
386 ))
387 })
388 .collect::<Result<Vec<_>>>()?;
389 format!("{{{}}}", entries.join(","))
390 }
391 _ => {
392 return Err(unsupported_shape(
393 "shape_json_schema exact expr lowering only supports json-like forms",
394 ));
395 }
396 })
397}
398
399fn json_string(text: &str) -> String {
400 format!("{text:?}")
401}
402
403fn unsupported_shape(message: impl Into<String>) -> Error {
404 Error::Eval(message.into())
405}
406
407#[cfg(test)]
408mod tests {
409 use std::sync::Arc;
410
411 use sim_kernel::{Expr, NumberLiteral, Result, Symbol};
412
413 use super::{
414 GrammarDialect, GrammarPosition, GrammarRenderer, GrammarTarget, Production, TerminalAtom,
415 shape_grammar, shape_grammar_graph, shape_json_schema,
416 };
417 use crate::{
418 ExactExprShape, ExprKind, ExprKindShape, FieldShape, FieldSpec, ListShape,
419 NumberValueShape, OneOfShape,
420 };
421
422 #[test]
423 fn graph_lowers_non_trivial_object_shape() {
424 let graph = shape_grammar_graph(&record_shape()).unwrap();
425
426 let Production::Call { args, .. } = graph.root else {
427 panic!("field shape should lower to a call production");
428 };
429 assert_eq!(args.len(), 2);
430 assert!(graph.defs.is_empty());
431 assert!(graph.diagnostics.is_empty());
432 }
433
434 #[test]
435 fn json_schema_renderer_preserves_seed_object_output() {
436 let grammar = shape_json_schema(&record_shape()).unwrap();
437
438 assert!(grammar.contains(r#""type":"object""#));
439 assert!(grammar.contains(r#""name":{"type":"string"}"#));
440 assert!(grammar.contains(r#""versions":{"type":"array""#));
441 assert!(grammar.contains(r#""additionalProperties":false"#));
442 }
443
444 #[test]
445 fn exact_expr_lowers_to_exact_terminal_and_const_schema() {
446 let expr = Expr::Map(vec![
447 (
448 Expr::Symbol(Symbol::new("head")),
449 Expr::String("ok".to_owned()),
450 ),
451 (
452 Expr::Symbol(Symbol::new("count")),
453 Expr::Number(NumberLiteral {
454 domain: Symbol::qualified("num", "int"),
455 canonical: "3".to_owned(),
456 }),
457 ),
458 ]);
459 let graph = shape_grammar_graph(&ExactExprShape::new(expr)).unwrap();
460
461 assert!(matches!(
462 graph.root,
463 Production::Terminal(TerminalAtom::Exact(_))
464 ));
465 assert_eq!(
466 shape_json_schema(&ExactExprShape::new(Expr::Bool(true))).unwrap(),
467 r#"{"const":true}"#,
468 );
469 }
470
471 #[test]
472 fn one_of_lowers_to_alt_and_any_remains_permissive() {
473 let shape = OneOfShape::new(vec![
474 Arc::new(ExprKindShape::new(ExprKind::Number)),
475 Arc::new(ExprKindShape::new(ExprKind::String)),
476 ]);
477 let graph = shape_grammar_graph(&shape).unwrap();
478
479 assert!(matches!(graph.root, Production::Alt(_)));
480 assert_eq!(shape_json_schema(&crate::AnyShape).unwrap(), "true");
481 }
482
483 #[test]
484 fn shape_grammar_uses_supplied_renderer() {
485 let target = GrammarTarget {
486 codec: Symbol::qualified("codec", "test"),
487 dialect: GrammarDialect::SExpr,
488 position: GrammarPosition::Data,
489 };
490 let rendered = shape_grammar(
491 &record_shape(),
492 target.clone(),
493 &StubRenderer {
494 codec: target.codec.clone(),
495 dialect: target.dialect,
496 },
497 )
498 .unwrap();
499
500 assert_eq!(rendered.target, target);
501 assert!(rendered.text.contains("codec/test"));
502 assert!(rendered.text.contains("root=Call"));
503 assert!(rendered.diagnostics.is_empty());
504 }
505
506 #[test]
507 fn shape_grammar_fails_closed_on_renderer_mismatch() {
508 let target = GrammarTarget {
509 codec: Symbol::qualified("codec", "test"),
510 dialect: GrammarDialect::SExpr,
511 position: GrammarPosition::Data,
512 };
513 let wrong_codec = shape_grammar(
514 &record_shape(),
515 target.clone(),
516 &StubRenderer {
517 codec: Symbol::qualified("codec", "other"),
518 dialect: target.dialect,
519 },
520 )
521 .unwrap_err();
522 assert!(wrong_codec.to_string().contains("codec codec/other"));
523
524 let wrong_dialect = shape_grammar(
525 &record_shape(),
526 target.clone(),
527 &StubRenderer {
528 codec: target.codec.clone(),
529 dialect: GrammarDialect::JsonSchema,
530 },
531 )
532 .unwrap_err();
533 assert!(wrong_dialect.to_string().contains("dialect JsonSchema"));
534 }
535
536 #[test]
537 fn unsupported_shapes_fail_closed() {
538 let err = shape_grammar_graph(&NumberValueShape).unwrap_err();
539 assert!(err.to_string().contains("does not support this shape"));
540
541 let err = shape_grammar_graph(&ExprKindShape::new(ExprKind::Bytes)).unwrap_err();
542 assert!(err.to_string().contains("expr-kind bytes"));
543 }
544
545 fn record_shape() -> FieldShape {
546 FieldShape::anonymous(vec![
547 FieldSpec::required(
548 Symbol::new("name"),
549 Arc::new(ExprKindShape::new(ExprKind::String)),
550 ),
551 FieldSpec::required(
552 Symbol::new("versions"),
553 Arc::new(ListShape::new(vec![
554 Arc::new(ExprKindShape::new(ExprKind::String)),
555 Arc::new(ExprKindShape::new(ExprKind::String)),
556 ])),
557 ),
558 ])
559 }
560
561 struct StubRenderer {
562 codec: Symbol,
563 dialect: GrammarDialect,
564 }
565
566 impl GrammarRenderer for StubRenderer {
567 fn codec_symbol(&self) -> Symbol {
568 self.codec.clone()
569 }
570
571 fn dialect(&self) -> GrammarDialect {
572 self.dialect
573 }
574
575 fn render(&self, graph: &crate::GrammarGraph, position: GrammarPosition) -> Result<String> {
576 Ok(format!(
577 "codec={} dialect={:?} position={:?} root={}",
578 self.codec,
579 self.dialect,
580 position,
581 production_kind(&graph.root)
582 ))
583 }
584 }
585
586 fn production_kind(production: &Production) -> &'static str {
587 match production {
588 Production::Terminal(_) => "Terminal",
589 Production::Seq(_) => "Seq",
590 Production::Alt(_) => "Alt",
591 Production::Repeat { .. } => "Repeat",
592 Production::Call { .. } => "Call",
593 Production::Ref(_) => "Ref",
594 }
595 }
596}