sim-lib-topology 0.1.4

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

use sim_kernel::{Cx, DefaultFactory, Error, Expr, NoopEvalPolicy, NumberLiteral, Symbol, Value};
use sim_value::build::sym;

use crate::{
    Budget, BudgetExhausted, Edge, Graph, Node, PortMode, PortRef, Scheduler, SchedulerMode,
    graph_from_value, parse_graph,
};

mod browse;
mod compile;
mod control;
mod diagram;
mod instrument;
mod nonlinear;
mod package;
mod patch;
mod place;
mod reflect;
mod registry;
mod replay;
mod run_core;
mod site;
mod text;
mod validate;

#[test]
fn model_minimal_graph_uses_deterministic_defaults() {
    let graph = Graph::minimal("review-flow");

    assert_eq!(graph.name, Symbol::new("review-flow"));
    assert_eq!(graph.version, "0.1.0");
    assert_eq!(graph.api, "sim.topology.v3");
    assert!(graph.input.is_none());
    assert!(graph.output.is_none());
    assert!(graph.nodes.is_empty());
    assert!(graph.edges.is_empty());
    assert!(graph.cells.is_empty());
    assert!(graph.capabilities.is_empty());
    assert!(graph.metadata.is_empty());
    assert!(graph.tests.is_empty());
    assert_eq!(graph.scheduler, Scheduler::default());
    assert_eq!(graph.budget, Budget::default());
}

#[test]
fn model_scheduler_and_budget_defaults_are_bounded() {
    let scheduler = Scheduler::default();
    assert_eq!(scheduler.mode, SchedulerMode::Sequential);
    assert_eq!(scheduler.seed, None);
    assert_eq!(scheduler.max_concurrency, 1);
    assert!(scheduler.deterministic);

    let budget = Budget::default();
    assert_eq!(budget.max_steps, 256);
    assert_eq!(budget.max_node_visits, 64);
    assert_eq!(budget.max_edge_visits, 64);
    assert_eq!(budget.max_outputs, 64);
    assert_eq!(budget.max_child_runs, 16);
    assert_eq!(budget.deadline_ms, None);
    assert_eq!(budget.on_exhausted, BudgetExhausted::Fail);
}

#[test]
fn model_node_default_ports_follow_verbs() {
    let source = Node::named("source", "in");
    assert!(source.inputs.is_empty());
    assert_eq!(port_names(&source.outputs), ["out"]);

    let sink = Node::named("sink", "out");
    assert_eq!(port_names(&sink.inputs), ["in"]);
    assert!(sink.outputs.is_empty());

    let call = Node::named("step", "call");
    assert_eq!(port_names(&call.inputs), ["in"]);
    assert_eq!(port_names(&call.outputs), ["out", "error"]);
    assert!(call.outputs.iter().any(|port| !port.required));

    let wire = Node::named("wire", "wire");
    assert_eq!(port_names(&wire.inputs), ["in"]);
    assert_eq!(port_names(&wire.outputs), ["out"]);
    assert!(wire.inputs.iter().all(|port| port.mode == PortMode::Value));
}

#[test]
fn model_port_refs_and_edges_normalize_default_ports() {
    let from = PortRef::output("left");
    let to = PortRef::input("right");
    let edge = Edge::new(0, from.clone(), to.clone());

    assert_eq!(from.node.as_symbol(), &Symbol::new("left"));
    assert_eq!(from.port, Symbol::new("out"));
    assert_eq!(to.node.as_symbol(), &Symbol::new("right"));
    assert_eq!(to.port, Symbol::new("in"));
    assert_eq!(edge.id.0, 0);
    assert_eq!(edge.priority, 0);
    assert!(edge.when.is_none());
    assert!(edge.transform.is_none());
    assert!(edge.as_name.is_none());
    assert!(edge.max_visits.is_none());
    assert!(edge.buffer.is_none());
    assert!(edge.metadata.is_empty());
}

#[test]
fn model_cell_and_test_constructors_are_deterministic() {
    let cell = crate::Cell::new(Symbol::new("state"), Expr::Nil);
    assert_eq!(cell.name, Symbol::new("state"));
    assert!(cell.shape.is_none());
    assert!(cell.merge.is_none());
    assert!(!cell.private);

    let case = crate::GraphTest::new(Symbol::new("empty"), Expr::Nil, Expr::Bool(true));
    assert_eq!(case.name, Symbol::new("empty"));
    assert!(case.fixtures.is_empty());
}

#[test]
fn parse_minimal_map_graph() {
    let mut cx = test_cx();
    let graph = parse_expr(
        &mut cx,
        map(vec![("kind", sym("topology")), ("name", sym("map-flow"))]),
    );

    assert_eq!(graph.name, Symbol::new("map-flow"));
    assert_eq!(graph.api, "sim.topology.v3");
    assert!(graph.nodes.is_empty());
    assert!(graph.edges.is_empty());
}

#[test]
fn parse_minimal_list_graph() {
    let mut cx = test_cx();
    let graph = parse_expr(
        &mut cx,
        Expr::List(vec![
            Expr::Symbol(Symbol::qualified("topology", "graph")),
            kw("name"),
            sym("list-flow"),
            kw("nodes"),
            Expr::List(vec![
                Expr::List(vec![sym("in"), kw("verb"), sym("in")]),
                Expr::List(vec![sym("out"), kw("verb"), sym("out")]),
            ]),
            kw("edges"),
            Expr::List(vec![Expr::List(vec![sym("in"), sym("->"), sym("out")])]),
            kw("budget"),
            Expr::List(vec![kw("max-steps"), int(20)]),
        ]),
    );

    assert_eq!(graph.name, Symbol::new("list-flow"));
    assert_eq!(graph.nodes.len(), 2);
    assert_eq!(graph.edges.len(), 1);
    assert_eq!(graph.edges[0].from.node.as_symbol(), &Symbol::new("in"));
    assert_eq!(graph.edges[0].from.port, Symbol::new("out"));
    assert_eq!(graph.edges[0].to.node.as_symbol(), &Symbol::new("out"));
    assert_eq!(graph.edges[0].to.port, Symbol::new("in"));
    assert_eq!(graph.budget.max_steps, 20);
}

#[test]
fn parse_explicit_ports_and_preserves_unknown_node_options() {
    let mut cx = test_cx();
    let target = Expr::Call {
        operator: Box::new(sym("explode-if-evaluated")),
        args: Vec::new(),
    };
    let graph = parse_expr(
        &mut cx,
        map(vec![
            ("name", sym("ports-flow")),
            (
                "nodes",
                Expr::List(vec![map(vec![
                    ("id", sym("gate")),
                    ("verb", sym("branch")),
                    (
                        "in",
                        Expr::List(vec![map(vec![
                            ("name", sym("payload")),
                            ("mode", sym("stream")),
                            ("required", Expr::Bool(false)),
                        ])]),
                    ),
                    ("out", Expr::List(vec![sym("accepted"), sym("rejected")])),
                    ("target", target.clone()),
                    ("unknown-option", Expr::String("kept".to_owned())),
                ])]),
            ),
        ]),
    );

    let node = &graph.nodes[0];
    assert_eq!(node.id.as_symbol(), &Symbol::new("gate"));
    assert_eq!(node.inputs.len(), 1);
    assert_eq!(node.inputs[0].name, Symbol::new("payload"));
    assert_eq!(node.inputs[0].mode, PortMode::Stream);
    assert!(!node.inputs[0].required);
    assert_eq!(port_names(&node.outputs), ["accepted", "rejected"]);
    assert!(node.target.as_ref().unwrap().canonical_eq(&target));
    assert_eq!(node.options[0].0, Symbol::new("unknown_option"));
    assert!(
        node.options[0]
            .1
            .canonical_eq(&Expr::String("kept".to_owned()))
    );
}

#[test]
fn parse_cells_scheduler_and_budget() {
    let mut cx = test_cx();
    let graph = parse_expr(
        &mut cx,
        map(vec![
            ("name", sym("state-flow")),
            (
                "cells",
                Expr::List(vec![map(vec![
                    ("name", sym("state")),
                    ("initial", Expr::String("empty".to_owned())),
                    ("merge", sym("last")),
                    ("private", Expr::Bool(true)),
                ])]),
            ),
            (
                "scheduler",
                map(vec![
                    ("mode", sym("sequential")),
                    ("seed", int(7)),
                    ("max_concurrency", int(1)),
                    ("deterministic", Expr::Bool(true)),
                ]),
            ),
            (
                "budget",
                Expr::List(vec![
                    kw("max-node-visits"),
                    int(9),
                    kw("max-edge-visits"),
                    int(10),
                    kw("deadline-ms"),
                    Expr::Nil,
                    kw("on-exhausted"),
                    sym("partial"),
                ]),
            ),
        ]),
    );

    assert_eq!(graph.cells.len(), 1);
    assert_eq!(graph.cells[0].name, Symbol::new("state"));
    assert_eq!(graph.cells[0].merge, Some(Symbol::new("last")));
    assert!(graph.cells[0].private);
    assert_eq!(graph.scheduler.mode, SchedulerMode::Sequential);
    assert_eq!(graph.scheduler.seed, Some(7));
    assert_eq!(graph.budget.max_node_visits, 9);
    assert_eq!(graph.budget.max_edge_visits, 10);
    assert_eq!(graph.budget.deadline_ms, None);
    assert_eq!(graph.budget.on_exhausted, BudgetExhausted::Partial);
}

#[test]
fn parse_metadata_tests_and_capabilities() {
    let mut cx = test_cx();
    let graph = parse_expr(
        &mut cx,
        map(vec![
            ("name", sym("tested-flow")),
            ("capabilities", Expr::List(vec![sym("net"), sym("file")])),
            (
                "metadata",
                map(vec![("owner", Expr::String("ops".to_owned()))]),
            ),
            (
                "tests",
                Expr::List(vec![map(vec![
                    ("name", sym("smoke")),
                    ("input", Expr::String("in".to_owned())),
                    ("expect", Expr::String("out".to_owned())),
                    ("fixtures", map(vec![("agent", sym("fake"))])),
                ])]),
            ),
        ]),
    );

    assert_eq!(
        graph.capabilities,
        [Symbol::new("net"), Symbol::new("file")]
    );
    assert_eq!(graph.metadata[0].0, Symbol::new("owner"));
    assert_eq!(graph.tests.len(), 1);
    assert_eq!(graph.tests[0].name, Symbol::new("smoke"));
    assert_eq!(graph.tests[0].fixtures[0].0, Symbol::new("agent"));
}

#[test]
fn parse_graph_from_value_alias_uses_parser() {
    let mut cx = test_cx();
    let value = value_expr(&mut cx, map(vec![("name", sym("alias-flow"))]));
    let graph = graph_from_value(&mut cx, value).expect("parsed graph");

    assert_eq!(graph.name, Symbol::new("alias-flow"));
}

#[test]
fn parse_bad_port_ref_reports_graph_field_path() {
    let mut cx = test_cx();
    let value = value_expr(
        &mut cx,
        map(vec![
            ("name", sym("bad-flow")),
            (
                "edges",
                Expr::List(vec![map(vec![
                    ("from", Expr::String("a:b:c".to_owned())),
                    ("to", sym("out")),
                ])]),
            ),
        ]),
    );

    let error = parse_graph(&mut cx, value).expect_err("bad port ref should fail");
    let Error::Eval(message) = error else {
        panic!("unexpected parse error type: {error}");
    };
    assert!(message.contains("graph.edges[0].from"));
}

fn port_names(ports: &[crate::Port]) -> Vec<&str> {
    ports
        .iter()
        .map(|port| port.name.name.as_ref())
        .collect::<Vec<_>>()
}

fn test_cx() -> Cx {
    Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory))
}

fn parse_expr(cx: &mut Cx, expr: Expr) -> Graph {
    let value = value_expr(cx, expr);
    parse_graph(cx, value).expect("parsed graph")
}

fn value_expr(cx: &mut Cx, expr: Expr) -> Value {
    cx.factory().expr(expr).expect("expr value")
}

fn map(entries: Vec<(&str, Expr)>) -> Expr {
    Expr::Map(
        entries
            .into_iter()
            .map(|(key, value)| (sym(key), value))
            .collect(),
    )
}

fn kw(name: &str) -> Expr {
    Expr::Symbol(Symbol::new(format!(":{name}")))
}

fn int(value: i64) -> Expr {
    Expr::Number(NumberLiteral {
        domain: Symbol::new("i64"),
        canonical: value.to_string(),
    })
}