sim-lib-topology 0.1.2

Data-driven topology engine.
Documentation
use std::sync::{
    Arc,
    atomic::{AtomicU32, Ordering},
};

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

use crate::{
    Edge, EdgeId, Graph, Node, PortRef, TopologyCounterfactual, counterfactual_replay,
    replay_report, topology_reflect_run, topology_run_capability,
};

#[test]
fn replay_report_repeats_output_without_calling_target() {
    let mut cx = runtime_cx();
    let calls = register_counting_prefix(&mut cx, "count", "count:");
    let graph = call_graph("replay-count", "count");

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

    assert_eq!(report.output, Expr::String("count:seed".to_owned()));
    assert_eq!(replayed, report.output);
    assert_eq!(calls.load(Ordering::SeqCst), 1);
    assert_eq!(report.recorded_replies.len(), 1);
}

#[test]
fn replay_counterfactual_replace_target_changes_output() {
    let mut cx = runtime_cx();
    register_prefix(&mut cx, "a", "a:");
    register_prefix(&mut cx, "b", "b:");
    let graph = call_graph("replace-target", "a");
    let report = topology_reflect_run(&mut cx, &graph, Expr::String("seed".to_owned()))
        .expect("reflected run");

    let changed = counterfactual_replay(
        &mut cx,
        &report,
        TopologyCounterfactual::ReplaceTarget {
            node: Symbol::new("call"),
            target: Expr::Symbol(Symbol::qualified("test", "b")),
        },
    )
    .expect("counterfactual replay");

    assert_eq!(report.output, Expr::String("a:seed".to_owned()));
    assert_eq!(changed, Expr::String("b:seed".to_owned()));
}

#[test]
fn replay_counterfactual_disable_edge_changes_output() {
    let mut cx = runtime_cx();
    let graph = branch_graph("disable-edge", true, true);
    let report = topology_reflect_run(&mut cx, &graph, Expr::Bool(false)).expect("reflected run");

    let changed = counterfactual_replay(
        &mut cx,
        &report,
        TopologyCounterfactual::DisableEdge { edge: EdgeId(2) },
    )
    .expect("counterfactual replay");

    assert_eq!(report.output, Expr::Bool(false));
    assert_eq!(changed, Expr::Nil);
}

#[test]
fn replay_counterfactual_force_predicate_changes_output() {
    let mut cx = runtime_cx();
    let graph = branch_graph("force-predicate", false, false);
    let report = topology_reflect_run(&mut cx, &graph, Expr::String("seed".to_owned()))
        .expect("reflected run");

    let changed = counterfactual_replay(
        &mut cx,
        &report,
        TopologyCounterfactual::ForcePredicate {
            node: Symbol::new("gate"),
            result: true,
        },
    )
    .expect("counterfactual replay");

    assert_eq!(report.output, Expr::Nil);
    assert_eq!(changed, Expr::String("seed".to_owned()));
}

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

fn call_graph(name: &str, target: &str) -> Graph {
    let mut graph = Graph::minimal(name);
    let mut call = Node::named("call", "call");
    call.target = Some(Expr::Symbol(Symbol::qualified("test", target)));
    graph.nodes = vec![Node::named("in", "in"), call, Node::named("out", "out")];
    graph.edges = vec![
        Edge::new(0, PortRef::output("in"), PortRef::input("call")),
        Edge::new(1, PortRef::output("call"), PortRef::input("out")),
    ];
    graph
}

fn branch_graph(name: &str, false_to_out: bool, predicate: bool) -> Graph {
    let mut graph = Graph::minimal(name);
    let mut gate = Node::named("gate", "branch");
    gate.options
        .push((Symbol::new("when"), Expr::Bool(predicate)));
    graph.nodes = vec![Node::named("in", "in"), gate, Node::named("out", "out")];
    graph.edges = vec![
        Edge::new(0, PortRef::output("in"), PortRef::input("gate")),
        Edge::new(1, PortRef::named("gate", "true"), PortRef::input("out")),
    ];
    if false_to_out {
        graph.edges.push(Edge::new(
            2,
            PortRef::named("gate", "false"),
            PortRef::input("out"),
        ));
    }
    graph
}

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();
}

fn register_counting_prefix(cx: &mut Cx, name: &str, prefix: &'static str) -> Arc<AtomicU32> {
    let calls = Arc::new(AtomicU32::new(0));
    let value = cx
        .factory()
        .opaque(Arc::new(CountingPrefixFn {
            prefix,
            calls: calls.clone(),
        }))
        .unwrap();
    cx.registry_mut()
        .register_value(Symbol::qualified("test", name), value)
        .unwrap();
    calls
}

#[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> {
        prefix_call(cx, self.prefix, args)
    }
}

#[derive(Clone)]
struct CountingPrefixFn {
    prefix: &'static str,
    calls: Arc<AtomicU32>,
}

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

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

impl sim_kernel::ObjectCompat for CountingPrefixFn {
    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 CountingPrefixFn {
    fn call(&self, cx: &mut Cx, args: Args) -> sim_kernel::Result<Value> {
        self.calls.fetch_add(1, Ordering::SeqCst);
        prefix_call(cx, self.prefix, args)
    }
}

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