sim-nest 0.1.0

The SIM constellation umbrella facade: one crate that re-exports the runtime kernel, codecs, number domains, and libraries behind features.
Documentation
use std::sync::Arc;

use sim_kernel::{Args, Cx, DefaultFactory, EagerPolicy, Expr, Symbol, Value};

use crate::{
    install_skill_lib,
    lib_skill::{FixtureBehavior, FixtureSkillSpec, FixtureTransport, SkillCard},
    runtime::install_core_runtime,
    shape::{AnyShape, shape_value},
};

#[test]
fn skill_core_functions_have_browse_help_and_examples() {
    let mut cx = test_cx();
    install_skill_lib(&mut cx).unwrap();

    for symbol in skill_core_symbols() {
        let subject = symbol_value(&cx, symbol.clone());
        let card = call(&mut cx, Symbol::qualified("core", "browse"), vec![subject]);
        let card = expr(&mut cx, &card);
        let help = table_value(&card, "help").expect("help");
        assert!(
            matches!(help, Expr::Map(_)),
            "{symbol} should have fixed Help"
        );

        let subject = symbol_value(&cx, symbol.clone());
        let examples = call(
            &mut cx,
            Symbol::qualified("core", "examples"),
            vec![subject],
        );
        let examples = expr(&mut cx, &examples);
        let Expr::List(items) = examples else {
            panic!("{symbol} examples should be a list");
        };
        assert_eq!(items.len(), 1, "{symbol} should have one authored example");
    }
}

#[test]
fn bound_skill_card_is_reachable_from_root_browse_catalog() {
    let mut cx = test_cx();
    install_fixture(&mut cx, "math.add", "math", "add");

    let root = symbol_value(&cx, Symbol::qualified("browse", "catalog"));
    let target = symbol_value(&cx, Symbol::qualified("skill", "math.add"));
    let path = call(
        &mut cx,
        Symbol::qualified("core", "browse-path"),
        vec![root, target],
    );
    let path = expr(&mut cx, &path);
    let Expr::List(items) = path else {
        panic!("skill card should be reachable from root");
    };
    assert_eq!(
        items.last(),
        Some(&Expr::Symbol(Symbol::qualified("skill", "math.add")))
    );

    let target = symbol_value(&cx, Symbol::qualified("skill", "math.add"));
    let card = call(&mut cx, Symbol::qualified("core", "browse"), vec![target]);
    let card = expr(&mut cx, &card);
    assert_eq!(
        table_value(&card, "kind"),
        Some(&Expr::Symbol(Symbol::qualified("skill", "card")))
    );
}

#[test]
fn public_browse_card_does_not_expose_transport_handles() {
    let mut cx = test_cx();
    install_fixture(
        &mut cx,
        "safe.echo",
        "endpoint_ref_should_not_appear",
        "credential_scope_should_not_appear",
    );

    let subject = symbol_value(&cx, Symbol::qualified("skill", "safe.echo"));
    let card = call(&mut cx, Symbol::qualified("core", "browse"), vec![subject]);
    let card = expr(&mut cx, &card);

    assert!(!expr_contains_string(
        &card,
        "endpoint_ref_should_not_appear"
    ));
    assert!(!expr_contains_string(
        &card,
        "credential_scope_should_not_appear"
    ));
}

#[cfg(all(
    feature = "codec-lisp",
    feature = "codec-json",
    feature = "codec-binary",
    feature = "codec-binary-base64"
))]
#[test]
fn skill_card_expr_round_trips_through_installed_codecs() {
    let mut cx = test_cx();
    install_codecs(&mut cx);
    let card = skill_card("math.add", "math", "add");
    let expr = card.to_expr(&mut cx).unwrap();

    for codec in [
        Symbol::qualified("codec", "lisp"),
        Symbol::qualified("codec", "json"),
        Symbol::qualified("codec", "binary"),
        Symbol::qualified("codec", "binary-base64"),
    ] {
        let decoded = codec_roundtrip(&mut cx, &codec, &expr);
        assert!(
            decoded.canonical_eq(&expr),
            "codec {codec} changed SkillCard expr {decoded:?}"
        );
        let rebuilt = SkillCard::from_expr(&decoded).unwrap();
        assert!(rebuilt.to_expr(&mut cx).unwrap().canonical_eq(&decoded));
    }
}

fn test_cx() -> Cx {
    let mut cx = Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory));
    install_core_runtime(&mut cx);
    cx
}

fn install_fixture(cx: &mut Cx, id: &str, transport_id: &str, operation: &str) {
    let fixture = Arc::new(FixtureTransport::new(transport_id));
    fixture
        .insert(operation, FixtureBehavior::EchoArgs)
        .unwrap();
    crate::lib_skill::install_fixture_skill(cx, fixture, skill_card(id, transport_id, operation))
        .unwrap();
}

fn skill_card(id: &str, transport_id: &str, operation: &str) -> SkillCard {
    SkillCard::fixture(FixtureSkillSpec {
        id: id.to_owned(),
        symbol: Symbol::qualified("skill", id.to_owned()),
        title: "Fixture Skill".to_owned(),
        description: "Safe public fixture skill descriptor.".to_owned(),
        input_shape: any_shape(),
        output_shape: any_shape(),
        transport_id: transport_id.to_owned(),
        operation: operation.to_owned(),
    })
}

fn any_shape() -> Value {
    shape_value(Symbol::new("Any"), Arc::new(AnyShape))
}

fn skill_core_symbols() -> Vec<Symbol> {
    vec![
        Symbol::qualified("skill", "install"),
        Symbol::qualified("skill", "bind"),
        Symbol::qualified("skill", "list"),
        Symbol::qualified("skill", "card"),
        Symbol::qualified("skill", "call"),
    ]
}

fn call(cx: &mut Cx, symbol: Symbol, args: Vec<Value>) -> Value {
    cx.call_function(&symbol, Args::new(args))
        .unwrap_or_else(|err| panic!("{symbol} failed: {err}"))
}

fn symbol_value(cx: &Cx, symbol: Symbol) -> Value {
    cx.factory().symbol(symbol).unwrap()
}

fn expr(cx: &mut Cx, value: &Value) -> Expr {
    value.object().as_expr(cx).unwrap()
}

fn table_value<'a>(expr: &'a Expr, field: &str) -> Option<&'a Expr> {
    let Expr::Map(entries) = expr else {
        panic!("expected map");
    };
    let field = Symbol::new(field.to_owned());
    entries.iter().find_map(|(key, value)| match key {
        Expr::Symbol(symbol) if symbol == &field => Some(value),
        _ => None,
    })
}

fn expr_contains_string(expr: &Expr, needle: &str) -> bool {
    match expr {
        Expr::String(text) => text == needle,
        Expr::List(items) | Expr::Vector(items) | Expr::Set(items) => {
            items.iter().any(|item| expr_contains_string(item, needle))
        }
        Expr::Map(entries) => entries.iter().any(|(key, value)| {
            expr_contains_string(key, needle) || expr_contains_string(value, needle)
        }),
        Expr::Call { operator, args } => {
            expr_contains_string(operator, needle)
                || args.iter().any(|arg| expr_contains_string(arg, needle))
        }
        Expr::Infix { left, right, .. } => {
            expr_contains_string(left, needle) || expr_contains_string(right, needle)
        }
        Expr::Prefix { arg, .. } | Expr::Postfix { arg, .. } => expr_contains_string(arg, needle),
        Expr::Block(items) => items.iter().any(|item| expr_contains_string(item, needle)),
        Expr::Quote { expr, .. } | Expr::Extension { payload: expr, .. } => {
            expr_contains_string(expr, needle)
        }
        Expr::Annotated { expr, annotations } => {
            expr_contains_string(expr, needle)
                || annotations
                    .iter()
                    .any(|(_, value)| expr_contains_string(value, needle))
        }
        Expr::Nil
        | Expr::Bool(_)
        | Expr::Number(_)
        | Expr::Symbol(_)
        | Expr::Local(_)
        | Expr::Bytes(_) => false,
    }
}

#[cfg(all(
    feature = "codec-lisp",
    feature = "codec-json",
    feature = "codec-binary",
    feature = "codec-binary-base64"
))]
fn install_codecs(cx: &mut Cx) {
    let lisp = crate::codec_lisp::LispCodecLib::new(cx.registry_mut().fresh_codec_id()).unwrap();
    cx.load_lib(&lisp).unwrap();
    let json = crate::codec_json::JsonCodecLib::new(cx.registry_mut().fresh_codec_id());
    cx.load_lib(&json).unwrap();
    let binary = crate::codec_binary::BinaryCodecLib::new(cx.registry_mut().fresh_codec_id());
    cx.load_lib(&binary).unwrap();
    let binary_base64 =
        crate::codec_binary_base64::BinaryBase64CodecLib::new(cx.registry_mut().fresh_codec_id());
    cx.load_lib(&binary_base64).unwrap();
}

#[cfg(all(
    feature = "codec-lisp",
    feature = "codec-json",
    feature = "codec-binary",
    feature = "codec-binary-base64"
))]
fn codec_roundtrip(cx: &mut Cx, codec: &Symbol, expr: &Expr) -> Expr {
    let output =
        sim_codec::encode_with_codec(cx, codec, expr, sim_kernel::EncodeOptions::default())
            .unwrap();
    let input = match output {
        sim_codec::Output::Text(text) => sim_codec::Input::Text(text),
        sim_codec::Output::Bytes(bytes) => sim_codec::Input::Bytes(bytes),
    };
    sim_codec::decode_with_codec(cx, codec, input, sim_kernel::ReadPolicy::default()).unwrap()
}