sim-shape 0.1.3

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

use sim_kernel::{
    CaseId, Expr, FunctionId, HintMetadata, NumberLiteral, PreparedArgs, Symbol,
    shape_is_subshape_of,
};

use crate::{
    AnyShape, Bindings, CaptureShape, EffectfulShape, ExactExprShape, ExprKind, ExprKindShape,
    FunctionCase, FunctionObject, ListShape, OneOfShape, PrattShape, Shape,
};

use super::{FakePrattParser, cx, test_case_impl};

#[test]
fn expr_kind_shape_checks_values_and_exprs() {
    let mut cx = cx();
    let shape = ExprKindShape::new(ExprKind::String);
    let expr = Expr::String("ok".to_owned());
    assert!(shape.check_expr(&mut cx, &expr).unwrap().accepted);

    let value = cx.factory().string("ok".to_owned()).unwrap();
    assert!(shape.check_value(&mut cx, value).unwrap().accepted);
}

#[test]
fn expr_kind_rejection_carries_shape_hint() {
    let mut cx = cx();
    let shape = ExprKindShape::new(ExprKind::Number);
    let matched = shape
        .check_expr(&mut cx, &Expr::String("text".to_owned()))
        .unwrap();

    assert!(!matched.accepted);
    let hints = HintMetadata::collect_from_diagnostic(&matched.diagnostics[0]);
    assert_eq!(hints[0].kind, Symbol::qualified("shape-hint", "expected"));
    assert!(hints[0].radar_text().contains("string expression"));
}

#[test]
fn capture_rejection_adds_binding_hint() {
    let mut cx = cx();
    let shape = CaptureShape::new(
        Symbol::new("n"),
        Arc::new(ExprKindShape::new(ExprKind::Number)),
    );
    let matched = shape
        .check_expr(&mut cx, &Expr::String("text".to_owned()))
        .unwrap();

    assert!(!matched.accepted);
    let hints = HintMetadata::collect_from_diagnostic(&matched.diagnostics[0]);
    assert_eq!(hints[0].kind, Symbol::qualified("shape-hint", "binding"));
    assert!(hints[0].radar_text().contains("n"));
}

#[test]
fn callable_overload_rejection_reports_hints() {
    let mut cx = cx();
    let function = FunctionObject::new(
        FunctionId(7),
        Symbol::qualified("test", "only-number"),
        vec![FunctionCase {
            id: CaseId(1),
            name: Symbol::qualified("case", "number"),
            args: Arc::new(ListShape::new(vec![Arc::new(ExprKindShape::new(
                ExprKind::Number,
            ))])),
            result: None,
            demand: Vec::new(),
            priority: 0,
            implementation: test_case_impl,
        }],
    );
    let prepared = PreparedArgs::new(vec![cx.factory().string("text".to_owned()).unwrap()]);

    let err = match function.select_case(&mut cx, &prepared) {
        Ok(_) => panic!("expected no matching overload"),
        Err(err) => err,
    };
    let sim_kernel::Error::NoMatchingOverload { diagnostics, .. } = err else {
        panic!("expected no matching overload");
    };
    let overload = HintMetadata::collect_from_diagnostic(&diagnostics[0]);
    let callable = HintMetadata::collect_from_diagnostic(&diagnostics[2]);

    assert_eq!(
        overload[0].kind,
        Symbol::qualified("shape-hint", "overload-selection")
    );
    assert_eq!(
        callable[0].kind,
        Symbol::qualified("shape-hint", "callable-mismatch")
    );
}

#[test]
fn bindings_can_populate_context_env() {
    let mut cx = cx();
    let mut bindings = Bindings::new();
    bindings.bind_expr(Symbol::new("s"), Expr::String("hello".to_owned()));
    bindings.bind_value(Symbol::new("n"), cx.factory().bool(true).unwrap());
    bindings.into_env(&mut cx).unwrap();

    assert!(cx.env().get(&Symbol::new("s")).is_some());
    assert!(cx.env().get(&Symbol::new("n")).is_some());
}

#[test]
fn pratt_shape_parses_string_surface_before_matching() {
    let mut cx = cx();
    let shape = PrattShape::new(
        Arc::new(FakePrattParser),
        Arc::new(ExprKindShape::new(ExprKind::Infix)),
    );

    let matched = shape
        .check_expr(&mut cx, &Expr::String("1 + 2 * 3".to_owned()))
        .unwrap();

    assert!(matched.accepted);
}

#[test]
fn shape_crate_has_no_concrete_codec_dependency() {
    let manifest = include_str!("../../Cargo.toml");

    for line in manifest.lines().map(str::trim) {
        assert!(
            !line.starts_with("sim-codec-"),
            "sim-shape must not depend on concrete codec crates: {line}"
        );
    }
}

#[test]
fn one_of_shape_reports_success_when_any_branch_matches() {
    let mut cx = cx();
    let shape = OneOfShape::new(vec![
        Arc::new(ExprKindShape::new(ExprKind::Number)),
        Arc::new(AnyShape),
    ]);
    let matched = shape
        .check_expr(&mut cx, &Expr::String("fallback".to_owned()))
        .unwrap();
    assert!(matched.accepted);
}

#[test]
fn exact_expr_is_subshape_of_matching_expr_kind() {
    let mut cx = cx();
    let child = ExactExprShape::new(Expr::Number(NumberLiteral {
        domain: Symbol::qualified("numbers", "f64"),
        canonical: "1".to_owned(),
    }));
    let number_parent = ExprKindShape::new(ExprKind::Number);
    let string_parent = ExprKindShape::new(ExprKind::String);

    assert!(shape_is_subshape_of(&mut cx, &child, &number_parent).unwrap());
    assert!(!shape_is_subshape_of(&mut cx, &child, &string_parent).unwrap());
}

#[test]
fn one_of_shape_is_subshape_only_when_every_branch_is_subshape() {
    let mut cx = cx();
    let number_parent = ExprKindShape::new(ExprKind::Number);
    let number_or_number = OneOfShape::new(vec![
        Arc::new(ExactExprShape::new(Expr::Number(NumberLiteral {
            domain: Symbol::qualified("numbers", "f64"),
            canonical: "1".to_owned(),
        }))),
        Arc::new(ExactExprShape::new(Expr::Number(NumberLiteral {
            domain: Symbol::qualified("numbers", "f64"),
            canonical: "2".to_owned(),
        }))),
    ]);
    let number_or_string = OneOfShape::new(vec![
        Arc::new(ExprKindShape::new(ExprKind::Number)),
        Arc::new(ExprKindShape::new(ExprKind::String)),
    ]);

    assert!(shape_is_subshape_of(&mut cx, &number_or_number, &number_parent).unwrap());
    assert!(!shape_is_subshape_of(&mut cx, &number_or_string, &number_parent).unwrap());
}

#[test]
fn effectful_shape_is_not_subshape_of_inner_or_any() {
    let mut cx = cx();
    let effectful = EffectfulShape::new(Arc::new(AnyShape));

    assert!(!shape_is_subshape_of(&mut cx, &effectful, &AnyShape).unwrap());
}