sim-shape 0.1.0-rc.1

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

use sim_citizen::{CitizenLib, value_from_expr};
use sim_kernel::{
    CapabilitySet, Cx, DefaultFactory, Error, Expr, NoopEvalPolicy, ObjectEncoding, Symbol, Value,
    read_construct_capability,
};

use crate::{
    AcceptOnNoDiagnosticsHook, AndShape, AnyShape, ClassShape, DiscardOnDiagnosticPrefixHook,
    ExactExprShape, ExprKind, ExprKindShape, HookedShape, ListShape, NotShape, OrShape,
    RepeatShape, ScoreFloorHook, TableExtraPolicy, TableFieldSpec, TableShape, TraceMarkHook,
    VennShapeSet, accept_on_no_diagnostics_hook_class_symbol, and_shape_class_symbol,
    any_shape_class_symbol, class_shape_class_symbol,
    discard_on_diagnostic_prefix_hook_class_symbol, exact_expr_shape_class_symbol,
    expr_kind_shape_class_symbol, hook_value, hooked_shape_class_symbol, list_shape_class_symbol,
    not_shape_class_symbol, or_shape_class_symbol, repeat_shape_class_symbol,
    score_floor_hook_class_symbol, table_shape_class_symbol, trace_mark_hook_class_symbol,
    venn_shape_set_class_symbol,
};

fn cx() -> Cx {
    let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
    cx.load_lib(&CitizenLib::all()).unwrap();
    cx
}

fn constructor(value: &Value, cx: &mut Cx) -> (Symbol, Vec<Expr>) {
    let encoding = value
        .object()
        .as_object_encoder()
        .expect("shape citizen must expose object encoder")
        .object_encoding(cx)
        .unwrap();
    let ObjectEncoding::Constructor { class, args } = encoding else {
        panic!("shape citizen must use constructor encoding");
    };
    (class, args)
}

fn read_construct_value(cx: &mut Cx, class: Symbol, args: &[Expr]) -> Value {
    let values = args
        .iter()
        .map(|arg| value_from_expr(cx, arg))
        .collect::<sim_kernel::Result<Vec<_>>>()
        .unwrap();
    cx.grant(read_construct_capability());
    cx.read_construct(&class, values).unwrap()
}

fn shape_accepts_expr(value: &Value, cx: &mut Cx, expr: Expr) -> bool {
    value
        .object()
        .as_shape()
        .expect("value must remain a callable shape")
        .check_expr(cx, &expr)
        .unwrap()
        .accepted
}

fn shape_fixtures() -> Vec<(Symbol, Value, Expr)> {
    vec![
        (
            any_shape_class_symbol(),
            crate::shape_value_with_encoding(
                any_shape_class_symbol(),
                Arc::new(AnyShape),
                ObjectEncoding::Constructor {
                    class: any_shape_class_symbol(),
                    args: vec![Expr::Symbol(Symbol::new("v1"))],
                },
            ),
            Expr::String("anything".to_owned()),
        ),
        (
            exact_expr_shape_class_symbol(),
            crate::shape_value_with_encoding(
                exact_expr_shape_class_symbol(),
                Arc::new(ExactExprShape::new(Expr::Bool(true))),
                ObjectEncoding::Constructor {
                    class: exact_expr_shape_class_symbol(),
                    args: vec![Expr::Symbol(Symbol::new("v1")), Expr::Bool(true)],
                },
            ),
            Expr::Bool(true),
        ),
        (
            expr_kind_shape_class_symbol(),
            crate::shape_value_with_encoding(
                expr_kind_shape_class_symbol(),
                Arc::new(ExprKindShape::new(ExprKind::Number)),
                ObjectEncoding::Constructor {
                    class: expr_kind_shape_class_symbol(),
                    args: vec![
                        Expr::Symbol(Symbol::new("v1")),
                        Expr::Symbol(Symbol::new("number")),
                    ],
                },
            ),
            Expr::Number(sim_kernel::NumberLiteral {
                domain: Symbol::qualified("numbers", "f64"),
                canonical: "1".to_owned(),
            }),
        ),
        (
            class_shape_class_symbol(),
            crate::shape_value_with_encoding(
                class_shape_class_symbol(),
                Arc::new(ClassShape::new(any_shape_class_symbol())),
                ObjectEncoding::Constructor {
                    class: class_shape_class_symbol(),
                    args: vec![
                        Expr::Symbol(Symbol::new("v1")),
                        Expr::Symbol(any_shape_class_symbol()),
                    ],
                },
            ),
            Expr::Symbol(any_shape_class_symbol()),
        ),
        (
            list_shape_class_symbol(),
            crate::shape_value_with_encoding(
                list_shape_class_symbol(),
                Arc::new(ListShape::new(vec![Arc::new(AnyShape)])),
                ObjectEncoding::Constructor {
                    class: list_shape_class_symbol(),
                    args: vec![
                        Expr::Symbol(Symbol::new("v1")),
                        Expr::List(vec![Expr::Call {
                            operator: Box::new(Expr::Symbol(any_shape_class_symbol())),
                            args: vec![Expr::Symbol(Symbol::new("v1"))],
                        }]),
                        Expr::Nil,
                    ],
                },
            ),
            Expr::List(vec![Expr::Bool(true)]),
        ),
        (
            table_shape_class_symbol(),
            crate::shape_value_with_encoding(
                table_shape_class_symbol(),
                Arc::new(TableShape::new(
                    vec![TableFieldSpec {
                        key: Symbol::new("ok"),
                        required: true,
                        shape: Arc::new(AnyShape),
                    }],
                    TableExtraPolicy::Reject,
                )),
                ObjectEncoding::Constructor {
                    class: table_shape_class_symbol(),
                    args: vec![
                        Expr::Symbol(Symbol::new("v1")),
                        Expr::List(vec![Expr::List(vec![
                            Expr::Symbol(Symbol::new("ok")),
                            Expr::Bool(true),
                            Expr::Call {
                                operator: Box::new(Expr::Symbol(any_shape_class_symbol())),
                                args: vec![Expr::Symbol(Symbol::new("v1"))],
                            },
                        ])]),
                        Expr::Symbol(Symbol::new("reject")),
                    ],
                },
            ),
            Expr::Map(vec![(Expr::Symbol(Symbol::new("ok")), Expr::Bool(true))]),
        ),
        (
            or_shape_class_symbol(),
            crate::shape_value_with_encoding(
                or_shape_class_symbol(),
                Arc::new(OrShape::new(vec![
                    Arc::new(ExactExprShape::new(Expr::Bool(false))),
                    Arc::new(ExactExprShape::new(Expr::Bool(true))),
                ])),
                ObjectEncoding::Constructor {
                    class: or_shape_class_symbol(),
                    args: vec![
                        Expr::Symbol(Symbol::new("v1")),
                        Expr::List(vec![
                            Expr::Call {
                                operator: Box::new(Expr::Symbol(exact_expr_shape_class_symbol())),
                                args: vec![Expr::Symbol(Symbol::new("v1")), Expr::Bool(false)],
                            },
                            Expr::Call {
                                operator: Box::new(Expr::Symbol(exact_expr_shape_class_symbol())),
                                args: vec![Expr::Symbol(Symbol::new("v1")), Expr::Bool(true)],
                            },
                        ]),
                        Expr::Symbol(Symbol::new("first-match")),
                    ],
                },
            ),
            Expr::Bool(true),
        ),
        (
            and_shape_class_symbol(),
            crate::shape_value_with_encoding(
                and_shape_class_symbol(),
                Arc::new(AndShape::new(vec![
                    Arc::new(AnyShape),
                    Arc::new(ExactExprShape::new(Expr::Bool(true))),
                ])),
                ObjectEncoding::Constructor {
                    class: and_shape_class_symbol(),
                    args: vec![
                        Expr::Symbol(Symbol::new("v1")),
                        Expr::List(vec![
                            Expr::Call {
                                operator: Box::new(Expr::Symbol(any_shape_class_symbol())),
                                args: vec![Expr::Symbol(Symbol::new("v1"))],
                            },
                            Expr::Call {
                                operator: Box::new(Expr::Symbol(exact_expr_shape_class_symbol())),
                                args: vec![Expr::Symbol(Symbol::new("v1")), Expr::Bool(true)],
                            },
                        ]),
                    ],
                },
            ),
            Expr::Bool(true),
        ),
        (
            not_shape_class_symbol(),
            crate::shape_value_with_encoding(
                not_shape_class_symbol(),
                Arc::new(NotShape::new(Arc::new(ExactExprShape::new(Expr::Bool(
                    false,
                ))))),
                ObjectEncoding::Constructor {
                    class: not_shape_class_symbol(),
                    args: vec![
                        Expr::Symbol(Symbol::new("v1")),
                        Expr::Call {
                            operator: Box::new(Expr::Symbol(exact_expr_shape_class_symbol())),
                            args: vec![Expr::Symbol(Symbol::new("v1")), Expr::Bool(false)],
                        },
                    ],
                },
            ),
            Expr::Bool(true),
        ),
        (
            repeat_shape_class_symbol(),
            crate::shape_value_with_encoding(
                repeat_shape_class_symbol(),
                Arc::new(RepeatShape::with_bounds(Arc::new(AnyShape), 1, Some(2))),
                ObjectEncoding::Constructor {
                    class: repeat_shape_class_symbol(),
                    args: vec![
                        Expr::Symbol(Symbol::new("v1")),
                        Expr::Call {
                            operator: Box::new(Expr::Symbol(any_shape_class_symbol())),
                            args: vec![Expr::Symbol(Symbol::new("v1"))],
                        },
                        Expr::Number(sim_kernel::NumberLiteral {
                            domain: Symbol::qualified("citizen", "int"),
                            canonical: "1".to_owned(),
                        }),
                        Expr::Number(sim_kernel::NumberLiteral {
                            domain: Symbol::qualified("citizen", "int"),
                            canonical: "2".to_owned(),
                        }),
                    ],
                },
            ),
            Expr::Vector(vec![Expr::Bool(true)]),
        ),
        (
            hooked_shape_class_symbol(),
            crate::shape_value_with_encoding(
                hooked_shape_class_symbol(),
                Arc::new(HookedShape::new(
                    Arc::new(AnyShape),
                    vec![Arc::new(TraceMarkHook)],
                )),
                ObjectEncoding::Constructor {
                    class: hooked_shape_class_symbol(),
                    args: vec![
                        Expr::Symbol(Symbol::new("v1")),
                        Expr::Call {
                            operator: Box::new(Expr::Symbol(any_shape_class_symbol())),
                            args: vec![Expr::Symbol(Symbol::new("v1"))],
                        },
                        Expr::List(vec![Expr::Call {
                            operator: Box::new(Expr::Symbol(trace_mark_hook_class_symbol())),
                            args: vec![Expr::Symbol(Symbol::new("v1"))],
                        }]),
                    ],
                },
            ),
            Expr::String("hooked".to_owned()),
        ),
    ]
}

#[test]
fn built_in_shape_citizens_use_shape_namespace_and_v1() {
    let mut cx = cx();
    for (expected, value, _) in shape_fixtures() {
        let (class, args) = constructor(&value, &mut cx);
        assert_eq!(class, expected);
        assert_eq!(args.first(), Some(&Expr::Symbol(Symbol::new("v1"))));
    }
}

#[test]
fn shape_citizens_preserve_match_behavior_after_roundtrip() {
    let mut cx = cx();
    for (_expected, value, accepted_expr) in shape_fixtures() {
        let (class, args) = constructor(&value, &mut cx);
        let decoded = read_construct_value(&mut cx, class, &args);
        assert!(decoded.object().as_callable().is_some());
        assert!(shape_accepts_expr(&value, &mut cx, accepted_expr.clone()));
        assert!(shape_accepts_expr(&decoded, &mut cx, accepted_expr));
    }
}

#[test]
fn hook_and_venn_citizens_use_constructor_forms() {
    let mut cx = cx();
    let hooks = vec![
        (
            trace_mark_hook_class_symbol(),
            hook_value(Arc::new(TraceMarkHook)),
        ),
        (
            score_floor_hook_class_symbol(),
            hook_value(Arc::new(ScoreFloorHook::new(10))),
        ),
        (
            accept_on_no_diagnostics_hook_class_symbol(),
            hook_value(Arc::new(AcceptOnNoDiagnosticsHook)),
        ),
        (
            discard_on_diagnostic_prefix_hook_class_symbol(),
            hook_value(Arc::new(DiscardOnDiagnosticPrefixHook::new("shape:"))),
        ),
    ];
    for (expected, value) in hooks {
        let (class, args) = constructor(&value, &mut cx);
        assert_eq!(class, expected);
        assert_eq!(args.first(), Some(&Expr::Symbol(Symbol::new("v1"))));
    }

    let venn = cx
        .factory()
        .opaque(Arc::new(VennShapeSet::new(vec![(
            Symbol::new("any"),
            Arc::new(AnyShape),
        )])))
        .unwrap();
    let (class, args) = constructor(&venn, &mut cx);
    assert_eq!(class, venn_shape_set_class_symbol());
    assert_eq!(args.first(), Some(&Expr::Symbol(Symbol::new("v1"))));
}

#[test]
fn shape_read_construct_is_capability_gated() {
    let mut cx = cx();
    let version = cx.factory().symbol(Symbol::new("v1")).unwrap();
    let err = cx
        .with_capabilities(CapabilitySet::default(), |cx| {
            cx.read_construct(&any_shape_class_symbol(), vec![version])
        })
        .expect_err("shape read-construct must require read-construct capability");
    assert!(
        matches!(err, Error::CapabilityDenied { capability } if capability == read_construct_capability())
    );
}

#[test]
fn shape_citizens_pass_universal_conformance() {
    let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
    sim_citizen::run_registered_conformance(&mut cx).unwrap();
}