sim-codec-json 0.2.0

SIM workspace package for sim codec json.
Documentation
use std::sync::Arc;

use sim_codec::{DecodePosition, encode_with_codec, grammar_check};
use sim_codec_json::{JsonCodecLib, JsonGrammarRenderer};
use sim_codec_lisp::{LispCodecLib, LispGrammarRenderer};
use sim_kernel::{Cx, EncodeOptions, Expr, NumberLiteral, Symbol, testing::eager_cx};
use sim_shape::{
    ExprKind, ExprKindShape, GrammarDialect, GrammarGraph, GrammarPosition, GrammarRenderer,
    GrammarTarget, ListShape, OneOfShape, Production, Shape, ShapeDefRef, ShapeDefs, TerminalAtom,
    shape_grammar,
};

#[test]
fn generated_corpus_round_trips_through_checker() {
    for case in renderer_cases() {
        let mut cx = cx();
        for shape in [flat_shape(), recursive_shape()] {
            let grammar = render_shape(shape.as_ref(), &case);
            assert_eq!(grammar.target.codec, case.codec);
            assert_eq!(grammar.target.dialect, case.dialect);
            assert!(!grammar.text.is_empty());
            assert_eq!(grammar.graph, graph_for(shape.as_ref()));

            let generated = enumerate_graph(&grammar.graph, 4);
            assert!(
                !generated.is_empty(),
                "generated corpus for {} must not be empty",
                case.codec
            );
            for expr in generated {
                let text = encode_text(&mut cx, &case.codec, &expr);
                let check = grammar_check(
                    &mut cx,
                    shape.as_ref(),
                    &case.codec,
                    &text,
                    DecodePosition::Data,
                )
                .unwrap_or_else(|err| panic!("check {} text {text:?}: {err}", case.codec));
                assert!(
                    check.accepted,
                    "shape grammar example {expr:?} failed for {} with {:?}",
                    case.codec, check.diagnostics
                );
                assert!(check.decoded.is_some());
            }
        }
    }
}

#[test]
fn representative_members_are_generated_and_checked() {
    let representatives = [
        (
            flat_shape(),
            Expr::List(vec![text("text"), Expr::Bool(true)]),
        ),
        (recursive_shape(), generated_nested_node(3)),
    ];
    for case in renderer_cases() {
        let mut cx = cx();
        for (shape, expr) in &representatives {
            let grammar = render_shape(shape.as_ref(), &case);
            let generated = enumerate_graph(&grammar.graph, 4);
            assert!(
                generated.contains(expr),
                "representative {expr:?} missing from generated corpus for {}",
                case.codec
            );
            let text = encode_text(&mut cx, &case.codec, expr);
            let check = grammar_check(
                &mut cx,
                shape.as_ref(),
                &case.codec,
                &text,
                DecodePosition::Data,
            )
            .unwrap();
            assert!(check.accepted, "{:?}", check.diagnostics);
        }
    }
}

#[test]
fn decode_failures_and_wrong_shape_text_are_distinct_reports() {
    for case in renderer_cases() {
        let mut cx = cx();
        let shape = flat_shape();
        let decode_failure = grammar_check(
            &mut cx,
            shape.as_ref(),
            &case.codec,
            case.malformed,
            DecodePosition::Data,
        )
        .unwrap();
        assert!(!decode_failure.accepted);
        assert!(decode_failure.decoded.is_none());
        assert!(!decode_failure.diagnostics.is_empty());

        let wrong_shape = Expr::List(vec![text("alpha"), text("not-bool")]);
        let text = encode_text(&mut cx, &case.codec, &wrong_shape);
        let shape_failure = grammar_check(
            &mut cx,
            shape.as_ref(),
            &case.codec,
            &text,
            DecodePosition::Data,
        )
        .unwrap();
        assert!(!shape_failure.accepted);
        assert!(shape_failure.decoded.is_some());
        assert!(!shape_failure.diagnostics.is_empty());
    }
}

#[test]
fn recursive_corpus_agrees_at_and_past_depth_bound() {
    for case in renderer_cases() {
        let mut cx = cx();
        let shape = recursive_shape();

        let accepted = nested_node(4);
        let text = encode_text(&mut cx, &case.codec, &accepted);
        let check = grammar_check(
            &mut cx,
            shape.as_ref(),
            &case.codec,
            &text,
            DecodePosition::Data,
        )
        .unwrap();
        assert!(check.accepted, "{:?}", check.diagnostics);
    }

    let mut lisp_cx = cx();
    let shape = recursive_shape();
    let over_bound = nested_node(70);
    let text = encode_text(&mut lisp_cx, &q("codec", "lisp"), &over_bound);
    let check = grammar_check(
        &mut lisp_cx,
        shape.as_ref(),
        &q("codec", "lisp"),
        &text,
        DecodePosition::Data,
    )
    .unwrap();
    assert!(!check.accepted);
    assert!(check.decoded.is_some());
    assert!(
        check
            .diagnostics
            .iter()
            .any(|diagnostic| diagnostic.message.contains("recursion budget")),
        "{:?}",
        check.diagnostics
    );

    let mut json_cx = cx();
    let text = encode_text(&mut json_cx, &q("codec", "json"), &over_bound);
    let check = grammar_check(
        &mut json_cx,
        shape.as_ref(),
        &q("codec", "json"),
        &text,
        DecodePosition::Data,
    )
    .unwrap();
    assert!(!check.accepted);
    assert!(!check.diagnostics.is_empty());
}

struct RendererCase {
    codec: Symbol,
    dialect: GrammarDialect,
    malformed: &'static str,
    renderer: Box<dyn GrammarRenderer>,
}

fn renderer_cases() -> Vec<RendererCase> {
    vec![
        RendererCase {
            codec: q("codec", "json"),
            dialect: GrammarDialect::JsonSchema,
            malformed: "{",
            renderer: Box::new(JsonGrammarRenderer::json_schema()),
        },
        RendererCase {
            codec: q("codec", "lisp"),
            dialect: GrammarDialect::SExpr,
            malformed: "(",
            renderer: Box::new(LispGrammarRenderer::sexpr()),
        },
    ]
}

fn cx() -> Cx {
    let mut cx = eager_cx();
    sim_test_support::register_core_classes(&mut cx);
    sim_test_support::register_f64_number_domain(&mut cx);
    let json = JsonCodecLib::new(cx.registry_mut().fresh_codec_id());
    cx.load_lib(&json).unwrap();
    let lisp = LispCodecLib::new(cx.registry_mut().fresh_codec_id()).unwrap();
    cx.load_lib(&lisp).unwrap();
    cx
}

fn render_shape(shape: &dyn Shape, case: &RendererCase) -> sim_shape::ShapeGrammar {
    shape_grammar(
        shape,
        GrammarTarget {
            codec: case.codec.clone(),
            dialect: case.dialect,
            position: GrammarPosition::Data,
        },
        case.renderer.as_ref(),
    )
    .unwrap()
}

fn graph_for(shape: &dyn Shape) -> GrammarGraph {
    sim_shape::shape_grammar_graph(shape).unwrap()
}

fn flat_shape() -> Arc<dyn Shape> {
    Arc::new(ListShape::new(vec![string_shape(), bool_shape()]))
}

fn recursive_shape() -> Arc<dyn Shape> {
    let node = q("shape", "Node");
    let node_ref: Arc<dyn Shape> = Arc::new(ShapeDefRef::new(node.clone()));
    let recursive_list: Arc<dyn Shape> =
        Arc::new(ListShape::new(vec![string_shape(), node_ref.clone()]));
    let node_shape: Arc<dyn Shape> =
        Arc::new(OneOfShape::new(vec![string_shape(), recursive_list]));
    Arc::new(ShapeDefs::new(
        Arc::new(ShapeDefRef::new(node.clone())),
        vec![(node, node_shape)],
    ))
}

fn string_shape() -> Arc<dyn Shape> {
    Arc::new(ExprKindShape::new(ExprKind::String))
}

fn bool_shape() -> Arc<dyn Shape> {
    Arc::new(ExprKindShape::new(ExprKind::Bool))
}

fn enumerate_graph(graph: &GrammarGraph, depth: usize) -> Vec<Expr> {
    enumerate_production(&graph.root, graph, depth)
}

fn enumerate_production(production: &Production, graph: &GrammarGraph, depth: usize) -> Vec<Expr> {
    match production {
        Production::Terminal(atom) => terminal_examples(atom),
        Production::Seq(items) => sequence_examples(items, graph, depth),
        Production::Alt(choices) => choices
            .iter()
            .flat_map(|choice| enumerate_production(choice, graph, depth))
            .collect(),
        Production::Repeat { inner, at_least } => {
            if *at_least == 0 {
                vec![Expr::List(Vec::new())]
            } else {
                enumerate_production(inner, graph, depth)
            }
        }
        Production::Call { head, args } => {
            let mut items = enumerate_production(head, graph, depth);
            items.extend(sequence_items(args, graph, depth));
            vec![Expr::List(items)]
        }
        Production::Ref(name) if depth > 0 => graph
            .defs
            .iter()
            .find(|(candidate, _)| candidate == name)
            .map(|(_, production)| enumerate_production(production, graph, depth - 1))
            .unwrap_or_default(),
        Production::Ref(_) => Vec::new(),
    }
}

fn sequence_examples(items: &[Production], graph: &GrammarGraph, depth: usize) -> Vec<Expr> {
    let variants = items
        .iter()
        .map(|item| enumerate_production(item, graph, depth))
        .collect::<Vec<_>>();
    cartesian_items(&variants)
        .into_iter()
        .map(Expr::List)
        .collect()
}

fn sequence_items(items: &[Production], graph: &GrammarGraph, depth: usize) -> Vec<Expr> {
    cartesian_items(
        &items
            .iter()
            .map(|item| enumerate_production(item, graph, depth))
            .collect::<Vec<_>>(),
    )
    .into_iter()
    .next()
    .unwrap_or_default()
}

fn cartesian_items(variants: &[Vec<Expr>]) -> Vec<Vec<Expr>> {
    let mut out = vec![Vec::new()];
    for choices in variants {
        let mut next = Vec::new();
        for prefix in &out {
            for choice in choices {
                let mut item = prefix.clone();
                item.push(choice.clone());
                next.push(item);
            }
        }
        out = next;
    }
    out
}

fn terminal_examples(atom: &TerminalAtom) -> Vec<Expr> {
    vec![match atom {
        TerminalAtom::Any => text("any"),
        TerminalAtom::Symbol => Expr::Symbol(q("demo", "symbol")),
        TerminalAtom::String => text("text"),
        TerminalAtom::Number => Expr::Number(NumberLiteral {
            domain: q("numbers", "f64"),
            canonical: "1.0".to_owned(),
        }),
        TerminalAtom::Bool => Expr::Bool(true),
        TerminalAtom::Nil => Expr::Nil,
        TerminalAtom::List => Expr::List(Vec::new()),
        TerminalAtom::Map => Expr::Map(Vec::new()),
        TerminalAtom::Exact(expr) => expr.clone(),
    }]
}

fn encode_text(cx: &mut Cx, codec: &Symbol, expr: &Expr) -> String {
    encode_with_codec(cx, codec, expr, EncodeOptions::default())
        .unwrap()
        .into_text()
        .unwrap()
}

fn nested_node(depth: usize) -> Expr {
    if depth == 0 {
        text("leaf")
    } else {
        Expr::List(vec![text("node"), nested_node(depth - 1)])
    }
}

fn generated_nested_node(depth: usize) -> Expr {
    if depth == 0 {
        text("text")
    } else {
        Expr::List(vec![text("text"), generated_nested_node(depth - 1)])
    }
}

fn text(value: &str) -> Expr {
    Expr::String(value.to_owned())
}

fn q(namespace: &str, name: &str) -> Symbol {
    Symbol::qualified(namespace, name)
}