sim-lib-topology 0.1.0

Data-driven topology engine.
Documentation
use sim_kernel::{Error, Symbol};

use super::{Graph, Node, PortRef, sym, test_cx};
use crate::{Edge, EdgeId, NodeId, compile_graph};

#[test]
fn compile_builds_stable_indexes_and_edge_lists() {
    let mut cx = test_cx();
    let graph = priority_graph();

    let compiled = compile_graph(&mut cx, &graph).expect("compiled graph");

    assert_eq!(compiled.name, Symbol::new("compile-flow"));
    assert_eq!(compiled.nodes.len(), 4);
    assert_eq!(compiled.edges.len(), 4);
    assert_eq!(compiled.node_index_by_id[&NodeId::from("in")], 0);
    assert_eq!(compiled.node_index_by_id[&NodeId::from("step")], 1);
    assert_eq!(compiled.edge_index_by_id[&EdgeId::from(1)], 1);
    assert_eq!(compiled.input_nodes, [0]);
    assert_eq!(compiled.output_nodes, [3]);
    assert_eq!(compiled.incoming_edges[3], [1, 3]);
    assert_eq!(compiled.outgoing_edges[1], [2, 1]);
    assert_eq!(compiled.edges[2].priority, -2);
    assert_eq!(compiled.edges[1].priority, 5);
}

#[test]
fn compile_is_deterministic_for_same_graph_twice() {
    let mut cx = test_cx();
    let graph = priority_graph();

    let first = compile_graph(&mut cx, &graph).expect("first compile");
    let second = compile_graph(&mut cx, &graph).expect("second compile");

    assert_eq!(first, second);
}

#[test]
fn compile_rejects_invalid_graph_before_plan() {
    let mut cx = test_cx();
    let graph = Graph::minimal("invalid-compile");

    let error = compile_graph(&mut cx, &graph).expect_err("compile should validate");
    let Error::Eval(message) = error else {
        panic!("unexpected compile error type: {error}");
    };
    assert!(message.contains("topology validation error"));
    assert!(message.contains("missing input"));
}

#[test]
fn compile_records_reachability_and_cycle_metadata() {
    let mut cx = test_cx();
    let graph = bounded_cycle_graph();

    let compiled = compile_graph(&mut cx, &graph).expect("compiled graph");

    assert_eq!(compiled.reachable_from_inputs, [true, true, true, true]);
    assert_eq!(compiled.cyclic_nodes, [false, true, true, false]);
    assert_eq!(compiled.cycle_edges, [false, true, true, false]);
}

fn priority_graph() -> Graph {
    let mut graph = Graph::minimal("compile-flow");
    let mut step = Node::named("step", "call");
    step.target = Some(sym("worker"));
    let mut side = Node::named("side", "call");
    side.target = Some(sym("side-worker"));
    graph.nodes = vec![
        Node::named("in", "in"),
        step,
        side,
        Node::named("out", "out"),
    ];

    let mut high_priority = Edge::new(1, PortRef::output("step"), PortRef::input("out"));
    high_priority.priority = 5;
    let mut low_priority = Edge::new(2, PortRef::output("step"), PortRef::input("side"));
    low_priority.priority = -2;
    graph.edges = vec![
        Edge::new(0, PortRef::output("in"), PortRef::input("step")),
        high_priority,
        low_priority,
        Edge::new(3, PortRef::output("side"), PortRef::input("out")),
    ];
    graph
}

fn bounded_cycle_graph() -> Graph {
    let mut graph = Graph::minimal("cycle-compile");
    let mut a = Node::named("a", "call");
    a.target = Some(sym("a-target"));
    let mut b = Node::named("b", "call");
    b.target = Some(sym("b-target"));
    graph.nodes = vec![Node::named("in", "in"), a, b, Node::named("out", "out")];

    let mut bounded_back_edge = Edge::new(2, PortRef::output("b"), PortRef::input("a"));
    bounded_back_edge.max_visits = Some(2);
    graph.edges = vec![
        Edge::new(0, PortRef::output("in"), PortRef::input("a")),
        Edge::new(1, PortRef::output("a"), PortRef::input("b")),
        bounded_back_edge,
        Edge::new(3, PortRef::output("b"), PortRef::input("out")),
    ];
    graph
}