sim-shape 0.1.0

Shape algebra, comparison, and match-hook helpers.
Documentation
use std::sync::Arc;

use sim_kernel::{Cx, Expr, NumberLiteral, Symbol};

use crate::{
    AndShape, AnyShape, CaptureShape, ExactExprShape, ExprKind, ExprKindShape, NotShape, OrShape,
    OrStrategy, RepeatShape, Shape, TableExtraPolicy, TableFieldSpec, TableShape,
};

use sim_kernel::testing::bare_cx as cx;

fn number_expr(text: &str) -> Expr {
    Expr::Number(NumberLiteral {
        domain: Symbol::qualified("numbers", "f64"),
        canonical: text.to_owned(),
    })
}

fn number_value(cx: &mut Cx, text: &str) -> sim_kernel::Value {
    cx.factory()
        .number_literal(Symbol::qualified("numbers", "f64"), text.to_owned())
        .unwrap()
}

#[test]
fn and_shape_accepts_when_all_children_accept() {
    let mut cx = cx();
    let shape = AndShape::new(vec![
        Arc::new(ExprKindShape::new(ExprKind::Number)),
        Arc::new(ExactExprShape::new(number_expr("1"))),
    ]);

    let matched = shape.check_expr(&mut cx, &number_expr("1")).unwrap();

    assert!(matched.accepted);
}

#[test]
fn and_shape_rejects_on_first_rejecting_child() {
    let mut cx = cx();
    let shape = AndShape::new(vec![
        Arc::new(ExprKindShape::new(ExprKind::Number)),
        Arc::new(ExprKindShape::new(ExprKind::String)),
        Arc::new(AnyShape),
    ]);

    let matched = shape.check_expr(&mut cx, &number_expr("1")).unwrap();

    assert!(!matched.accepted);
    assert!(matched.diagnostics[0].message.starts_with("shape-and:"));
}

#[test]
fn and_shape_merges_captures_from_accepted_children() {
    let mut cx = cx();
    let shape = AndShape::new(vec![
        Arc::new(CaptureShape::new(Symbol::new("a"), Arc::new(AnyShape))),
        Arc::new(CaptureShape::new(Symbol::new("b"), Arc::new(AnyShape))),
    ]);

    let matched = shape
        .check_expr(&mut cx, &Expr::String("ok".to_owned()))
        .unwrap();

    assert!(matched.accepted);
    assert_eq!(matched.captures.exprs().len(), 2);
}

#[test]
fn empty_and_shape_accepts() {
    let mut cx = cx();
    let shape = AndShape::new(Vec::new());

    let matched = shape
        .check_expr(&mut cx, &Expr::String("anything".to_owned()))
        .unwrap();

    assert!(matched.accepted);
    assert_eq!(matched.score.value(), 0);
}

#[test]
fn or_shape_returns_leftmost_branch_by_default() {
    let mut cx = cx();
    let shape = OrShape::new(vec![
        Arc::new(AnyShape),
        Arc::new(ExactExprShape::new(number_expr("1"))),
    ]);

    let matched = shape.check_expr(&mut cx, &number_expr("1")).unwrap();

    assert!(matched.accepted);
    assert_eq!(
        matched.captures.exprs()[0],
        (
            Symbol::qualified("shape", "branch-index"),
            crate::algebra::number_expr(0)
        )
    );
}

#[test]
fn or_shape_can_pick_best_score() {
    let mut cx = cx();
    let shape = OrShape::with_strategy(
        vec![
            Arc::new(ExprKindShape::new(ExprKind::Number)),
            Arc::new(ExactExprShape::new(number_expr("1"))),
        ],
        OrStrategy::BestScore,
    );

    let matched = shape.check_expr(&mut cx, &number_expr("1")).unwrap();

    assert!(matched.accepted);
    assert_eq!(
        matched.captures.exprs()[0],
        (
            Symbol::qualified("shape", "branch-index"),
            crate::algebra::number_expr(1)
        )
    );
}

#[test]
fn or_shape_rejects_with_collected_diagnostics() {
    let mut cx = cx();
    let shape = OrShape::new(vec![
        Arc::new(ExprKindShape::new(ExprKind::Number)),
        Arc::new(ExprKindShape::new(ExprKind::String)),
    ]);

    let matched = shape.check_expr(&mut cx, &Expr::Bool(true)).unwrap();

    assert!(!matched.accepted);
    assert!(matched.diagnostics[0].message.starts_with("shape-or:"));
    assert!(matched.diagnostics.len() >= 3);
}

#[test]
fn empty_or_shape_rejects() {
    let mut cx = cx();
    let shape = OrShape::new(Vec::new());

    let matched = shape.check_expr(&mut cx, &Expr::Bool(true)).unwrap();

    assert!(!matched.accepted);
    assert!(matched.diagnostics[0].message.starts_with("shape-or:"));
}

#[test]
fn not_shape_accepts_when_inner_rejects() {
    let mut cx = cx();
    let shape = NotShape::new(Arc::new(ExprKindShape::new(ExprKind::Number)));

    let matched = shape
        .check_expr(&mut cx, &Expr::String("ok".to_owned()))
        .unwrap();

    assert!(matched.accepted);
    assert_eq!(
        matched.captures.exprs()[0],
        (Symbol::qualified("shape", "negated"), Expr::Bool(true))
    );
}

#[test]
fn not_shape_rejects_when_inner_accepts() {
    let mut cx = cx();
    let shape = NotShape::new(Arc::new(AnyShape));

    let matched = shape.check_expr(&mut cx, &Expr::Bool(true)).unwrap();

    assert!(!matched.accepted);
    assert!(matched.diagnostics[0].message.starts_with("shape-not:"));
}

#[test]
fn not_shape_does_not_leak_inner_captures() {
    let mut cx = cx();
    let shape = NotShape::new(Arc::new(CaptureShape::new(
        Symbol::new("inner"),
        Arc::new(ExprKindShape::new(ExprKind::Number)),
    )));

    let matched = shape
        .check_expr(&mut cx, &Expr::String("ok".to_owned()))
        .unwrap();

    assert!(matched.accepted);
    assert_eq!(matched.captures.exprs().len(), 1);
    assert_eq!(
        matched.captures.exprs()[0].0,
        Symbol::qualified("shape", "negated")
    );
}

#[test]
fn table_shape_accepts_required_keys() {
    let mut cx = cx();
    let shape = TableShape::single(
        Symbol::new("n"),
        Arc::new(ExprKindShape::new(ExprKind::Number)),
    );
    let value = number_value(&mut cx, "1");
    let table = cx.factory().table(vec![(Symbol::new("n"), value)]).unwrap();

    let matched = shape.check_value(&mut cx, table).unwrap();

    assert!(matched.accepted);
}

#[test]
fn table_shape_rejects_missing_required_keys() {
    let mut cx = cx();
    let shape = TableShape::single(Symbol::new("n"), Arc::new(AnyShape));
    let table = cx.factory().table(Vec::new()).unwrap();

    let matched = shape.check_value(&mut cx, table).unwrap();

    assert!(!matched.accepted);
    assert!(matched.diagnostics[0].message.starts_with("shape-table:"));
}

#[test]
fn table_shape_rejects_extra_keys_under_closed_policy() {
    let mut cx = cx();
    let shape = TableShape::new(
        vec![TableFieldSpec {
            key: Symbol::new("n"),
            shape: Arc::new(AnyShape),
            required: true,
        }],
        TableExtraPolicy::Reject,
    );
    let required = number_value(&mut cx, "1");
    let extra = number_value(&mut cx, "2");
    let table = cx
        .factory()
        .table(vec![
            (Symbol::new("n"), required),
            (Symbol::new("extra"), extra),
        ])
        .unwrap();

    let matched = shape.check_value(&mut cx, table).unwrap();

    assert!(!matched.accepted);
    assert!(matched.diagnostics[0].message.starts_with("shape-table:"));
}

#[test]
fn table_shape_checks_extra_values_under_shape_policy() {
    let mut cx = cx();
    let shape = TableShape::new(
        vec![TableFieldSpec {
            key: Symbol::new("n"),
            shape: Arc::new(AnyShape),
            required: true,
        }],
        TableExtraPolicy::Shape(Arc::new(ExprKindShape::new(ExprKind::Number))),
    );
    let required = number_value(&mut cx, "1");
    let extra = number_value(&mut cx, "2");
    let table = cx
        .factory()
        .table(vec![
            (Symbol::new("n"), required),
            (Symbol::new("extra"), extra),
        ])
        .unwrap();

    let matched = shape.check_value(&mut cx, table).unwrap();

    assert!(matched.accepted);
}

#[test]
fn repeat_shape_accepts_zero_items_when_min_is_zero() {
    let mut cx = cx();
    let shape = RepeatShape::new(Arc::new(AnyShape));

    let matched = shape.check_expr(&mut cx, &Expr::List(Vec::new())).unwrap();

    assert!(matched.accepted);
}

#[test]
fn repeat_shape_rejects_too_few_items() {
    let mut cx = cx();
    let shape = RepeatShape::with_bounds(Arc::new(AnyShape), 2, None);

    let matched = shape
        .check_expr(&mut cx, &Expr::List(vec![Expr::Bool(true)]))
        .unwrap();

    assert!(!matched.accepted);
    assert!(matched.diagnostics[0].message.starts_with("shape-repeat:"));
}

#[test]
fn repeat_shape_rejects_too_many_items() {
    let mut cx = cx();
    let shape = RepeatShape::with_bounds(Arc::new(AnyShape), 0, Some(1));

    let matched = shape
        .check_expr(
            &mut cx,
            &Expr::List(vec![Expr::Bool(true), Expr::Bool(false)]),
        )
        .unwrap();

    assert!(!matched.accepted);
    assert!(matched.diagnostics[0].message.starts_with("shape-repeat:"));
}

#[test]
fn repeat_shape_binds_repeat_count() {
    let mut cx = cx();
    let shape = RepeatShape::new(Arc::new(AnyShape));

    let matched = shape
        .check_expr(
            &mut cx,
            &Expr::Vector(vec![Expr::Bool(true), Expr::Bool(false)]),
        )
        .unwrap();

    assert!(matched.accepted);
    assert_eq!(
        matched.captures.exprs()[0],
        (
            Symbol::qualified("shape", "repeat-count"),
            crate::algebra::number_expr(2)
        )
    );
}