Skip to main content

sim_lib_view_agent/
editor.rs

1//! The composer editor: composer Intents applied to a `Graph`.
2//!
3//! Each composer Intent (create/move/wire/unwire/delete) maps to one or more
4//! `sim-lib-topology` `PatchOp`s and is applied through `apply_topology_patch_ops`,
5//! producing a new `Graph`. There is no second model: the topology patch engine
6//! is the one that mutates topologies. Because the Intent already carries an
7//! `origin.operator`, a human and an agent edit the same topology through the
8//! same path; only the recorded operator differs.
9
10use sim_kernel::{CapabilityName, Cx, Error, Expr, Result, Symbol};
11use sim_lib_intent::{field, intent_kind_of};
12use sim_lib_topology::{
13    Edge, EdgeId, Graph, Node, NodeId, PatchOp, PortRef, TopologyPatch, apply_topology_patch_ops,
14};
15
16/// Apply a composer Intent to `graph`, returning the new graph.
17///
18/// Editing a topology is a checked operation: the caller must hold the
19/// `topology-write` capability, gated exactly like any other writer. The
20/// composer applies structural ops without recompiling, because a topology
21/// under construction is legitimately incomplete; full validation runs at save
22/// or run.
23pub fn apply_composer_intent(cx: &mut Cx, graph: &Graph, intent: &Expr) -> Result<Graph> {
24    cx.require(&CapabilityName::new("topology-write"))?;
25    let ops = composer_ops(graph, intent)?;
26    let patch = TopologyPatch { ops };
27    apply_topology_patch_ops(graph, &patch)
28}
29
30fn composer_ops(graph: &Graph, intent: &Expr) -> Result<Vec<PatchOp>> {
31    let kind = intent_kind_of(intent)
32        .ok_or_else(|| Error::HostError("composer input is not an Intent".to_owned()))?;
33    match &*kind.name {
34        "create" => {
35            let verb = require_symbol(intent, "class")?;
36            let id = create_id(graph, intent, &verb);
37            let mut ops = vec![PatchOp::AddNode(Node::named(
38                NodeId(id.clone()),
39                verb.name.to_string(),
40            ))];
41            if let Some(at) = field(intent, "at") {
42                ops.push(PatchOp::SetMetadata {
43                    key: pos_key(&id),
44                    value: at.clone(),
45                });
46            }
47            Ok(ops)
48        }
49        "move" => {
50            let node = require_symbol(intent, "node")?;
51            let at = field(intent, "at").cloned().unwrap_or(Expr::Nil);
52            Ok(vec![PatchOp::SetMetadata {
53                key: pos_key(&node),
54                value: at,
55            }])
56        }
57        "wire" => {
58            let from = port_ref(field(intent, "from"))?;
59            let to = port_ref(field(intent, "to"))?;
60            Ok(vec![PatchOp::AddEdge {
61                edge: Edge::new(EdgeId(0), from, to),
62                explicit_id: false,
63            }])
64        }
65        "unwire" => {
66            let edge = field(intent, "edge")
67                .ok_or_else(|| Error::HostError("unwire is missing an 'edge'".to_owned()))?;
68            let from = port_ref(sub_field(edge, "from"))?;
69            let to = port_ref(sub_field(edge, "to"))?;
70            Ok(vec![PatchOp::RemoveEdge { from, to }])
71        }
72        "delete" => {
73            let targets = match field(intent, "targets") {
74                Some(Expr::List(items)) => items,
75                _ => {
76                    return Err(Error::HostError(
77                        "delete 'targets' must be a list".to_owned(),
78                    ));
79                }
80            };
81            targets
82                .iter()
83                .map(|target| match target {
84                    Expr::Symbol(symbol) => Ok(PatchOp::RemoveNode(NodeId(symbol.clone()))),
85                    _ => Err(Error::HostError(
86                        "delete target must be a node id".to_owned(),
87                    )),
88                })
89                .collect()
90        }
91        other => Err(Error::HostError(format!(
92            "composer does not handle intent '{other}'"
93        ))),
94    }
95}
96
97fn create_id(graph: &Graph, intent: &Expr, verb: &Symbol) -> Symbol {
98    if let Some(args) = field(intent, "args")
99        && let Some(Expr::Symbol(id)) = sub_field(args, "id")
100    {
101        return id.clone();
102    }
103    Symbol::new(format!("{}{}", verb.name, graph.nodes.len()))
104}
105
106fn pos_key(node: &Symbol) -> Symbol {
107    Symbol::new(format!("pos:{}", node.name))
108}
109
110fn port_ref(field: Option<&Expr>) -> Result<PortRef> {
111    let map = field.ok_or_else(|| Error::HostError("missing a port reference".to_owned()))?;
112    let node = match sub_field(map, "node") {
113        Some(Expr::Symbol(symbol)) => symbol.clone(),
114        _ => {
115            return Err(Error::HostError(
116                "port ref 'node' must be a symbol".to_owned(),
117            ));
118        }
119    };
120    let port = match sub_field(map, "port") {
121        Some(Expr::Symbol(symbol)) => symbol.clone(),
122        _ => Symbol::new("out"),
123    };
124    Ok(PortRef::new(NodeId(node), port))
125}
126
127fn require_symbol(intent: &Expr, name: &str) -> Result<Symbol> {
128    match field(intent, name) {
129        Some(Expr::Symbol(symbol)) => Ok(symbol.clone()),
130        _ => Err(Error::HostError(format!(
131            "composer intent field '{name}' must be a symbol"
132        ))),
133    }
134}
135
136fn sub_field<'a>(map: &'a Expr, name: &str) -> Option<&'a Expr> {
137    let Expr::Map(entries) = map else {
138        return None;
139    };
140    entries.iter().find_map(|(key, value)| {
141        matches!(key, Expr::Symbol(symbol) if &*symbol.name == name && symbol.namespace.is_none())
142            .then_some(value)
143    })
144}