sim-lib-topology 0.1.2

Data-driven topology engine.
Documentation
use std::sync::Arc;

use sim_kernel::{
    Args, CORE_FUNCTION_CLASS_ID, Callable, ClassRef, Cx, DefaultFactory, EagerPolicy, Expr,
    Object, Symbol, Value,
};

use crate::{
    Cell, Edge, Graph, Node, PortRef, parse_graph, topology_explain, topology_reflect_capability,
    topology_reflect_graph, topology_reflect_run, topology_run_capability,
};

#[test]
fn reflect_graph_round_trips_canonical_data() {
    let mut cx = runtime_cx();
    let graph = in_out_graph("reflect-round-trip");
    let value = cx
        .factory()
        .expr(topology_reflect_graph(&cx, &graph))
        .expect("reflected graph value");

    let reparsed = parse_graph(&mut cx, value).expect("reparsed graph");

    assert_eq!(reparsed.name, Symbol::new("reflect-round-trip"));
    assert_eq!(reparsed.nodes.len(), 2);
    assert_eq!(reparsed.edges.len(), 1);
}

#[test]
fn reflect_explanation_includes_node_visits_and_edge_choices() {
    let mut cx = runtime_cx();
    let graph = in_out_graph("reflect-explain");

    let report = topology_reflect_run(&mut cx, &graph, Expr::String("seed".to_owned()))
        .expect("reflected run");
    let explanation = topology_explain(&report);

    assert_eq!(report.output, Expr::String("seed".to_owned()));
    assert!(!report.node_visits.is_empty());
    assert!(report.edge_visits.iter().any(|visit| visit.visits > 0));
    assert!(list_field(&explanation, "node-visits").is_some_and(|items| !items.is_empty()));
    assert!(list_field(&explanation, "edge-choices").is_some_and(|items| !items.is_empty()));
}

#[test]
fn reflect_redacts_private_cells_and_targets_without_capability() {
    let mut cx = runtime_cx();
    register_prefix(&mut cx, "secret", "secret:");
    let graph = private_cell_call_graph();

    let redacted = topology_reflect_run(&mut cx, &graph, Expr::String("seed".to_owned()))
        .expect("redacted report");
    let call_node = redacted
        .nodes
        .iter()
        .find(|node| node.id == Symbol::new("call"))
        .expect("call node");
    assert!(call_node.redacted);
    assert_eq!(
        call_node.target,
        Some(Expr::Symbol(Symbol::qualified("topology", "redacted")))
    );
    assert!(redacted.cells[0].redacted);
    assert_eq!(
        redacted.cells[0].value,
        Expr::Symbol(Symbol::qualified("topology", "redacted"))
    );

    cx.grant(topology_reflect_capability());
    let revealed = topology_reflect_run(&mut cx, &graph, Expr::String("seed".to_owned()))
        .expect("revealed report");
    let call_node = revealed
        .nodes
        .iter()
        .find(|node| node.id == Symbol::new("call"))
        .expect("call node");
    assert!(!call_node.redacted);
    assert_eq!(
        call_node.target,
        Some(Expr::Symbol(Symbol::qualified("test", "secret")))
    );
    assert!(!revealed.cells[0].redacted);
    assert_eq!(
        revealed.cells[0].value,
        Expr::List(vec![Expr::String("secret:seed".to_owned())])
    );
}

#[test]
fn reflect_history_keeps_bounded_length() {
    let mut cx = runtime_cx();
    let graph = in_out_graph("reflect-history");
    let first = topology_reflect_run(&mut cx, &graph, Expr::String("first".to_owned()))
        .expect("first report");
    let second = topology_reflect_run(&mut cx, &graph, Expr::String("second".to_owned()))
        .expect("second report");
    let mut history = crate::TopologyHistory::new(1);

    let first_id = history.record(first);
    let second_id = history.record(second);

    assert_eq!(history.run_ids(), vec![second_id]);
    assert!(history.get(first_id).is_none());
    assert_eq!(
        history.get(second_id).expect("second retained").output,
        Expr::String("second".to_owned())
    );
}

fn runtime_cx() -> Cx {
    let mut cx = Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory));
    cx.grant(topology_run_capability());
    cx
}

fn in_out_graph(name: &str) -> Graph {
    let mut graph = Graph::minimal(name);
    graph.nodes = vec![Node::named("in", "in"), Node::named("out", "out")];
    graph.edges = vec![Edge::new(0, PortRef::output("in"), PortRef::input("out"))];
    graph
}

fn private_cell_call_graph() -> Graph {
    let mut graph = Graph::minimal("private-cell-call");
    let mut call = Node::named("call", "call");
    call.target = Some(Expr::Symbol(Symbol::qualified("test", "secret")));
    let mut save = Node::named("save", "cell");
    save.options = vec![
        option("name", "secret-log"),
        option("op", "append"),
        option("emit", "cell"),
    ];
    let mut cell = Cell::new(Symbol::new("secret-log"), Expr::List(Vec::new()));
    cell.private = true;
    graph.nodes = vec![
        Node::named("in", "in"),
        call,
        save,
        Node::named("out", "out"),
    ];
    graph.edges = vec![
        Edge::new(0, PortRef::output("in"), PortRef::input("call")),
        Edge::new(1, PortRef::output("call"), PortRef::input("save")),
        Edge::new(2, PortRef::output("save"), PortRef::input("out")),
    ];
    graph.cells = vec![cell];
    graph
}

fn option(key: &str, value: &str) -> (Symbol, Expr) {
    (Symbol::new(key), Expr::Symbol(Symbol::new(value)))
}

fn list_field<'a>(expr: &'a Expr, name: &str) -> Option<&'a [Expr]> {
    let Expr::Map(entries) = expr else {
        return None;
    };
    entries.iter().find_map(|(key, value)| match (key, value) {
        (Expr::Symbol(symbol), Expr::List(items))
            if symbol.namespace.is_none() && symbol.name.as_ref() == name =>
        {
            Some(items.as_slice())
        }
        _ => None,
    })
}

fn register_prefix(cx: &mut Cx, name: &str, prefix: &'static str) {
    let value = cx.factory().opaque(Arc::new(PrefixFn { prefix })).unwrap();
    cx.registry_mut()
        .register_value(Symbol::qualified("test", name), value)
        .unwrap();
}

#[derive(Clone)]
struct PrefixFn {
    prefix: &'static str,
}

impl Object for PrefixFn {
    fn display(&self, _cx: &mut Cx) -> sim_kernel::Result<String> {
        Ok("#<function test/prefix>".to_owned())
    }

    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}

impl sim_kernel::ObjectCompat for PrefixFn {
    fn class(&self, cx: &mut Cx) -> sim_kernel::Result<ClassRef> {
        cx.factory().class_stub(
            CORE_FUNCTION_CLASS_ID,
            Symbol::qualified("core", "Function"),
        )
    }

    fn as_callable(&self) -> Option<&dyn Callable> {
        Some(self)
    }
}

impl Callable for PrefixFn {
    fn call(&self, cx: &mut Cx, args: Args) -> sim_kernel::Result<Value> {
        let Some(first) = args.values().first() else {
            return cx.factory().string(self.prefix.to_owned());
        };
        let Expr::String(text) = first.object().as_expr(cx)? else {
            return cx.factory().string(self.prefix.to_owned());
        };
        cx.factory().string(format!("{}{text}", self.prefix))
    }
}