Skip to main content

panproto_inst/
tree_edit.rs

1//! Edit algebra for W-type instances (model of `ThEditableStructure`).
2//!
3//! A [`TreeEdit`] is an element of the edit monoid for tree-shaped
4//! instances. The monoid operations are [`TreeEdit::identity`],
5//! [`TreeEdit::compose`], and [`TreeEdit::apply`] (the partial monoid
6//! action on [`WInstance`]).
7//!
8//! Each variant corresponds to a primitive mutation on a W-type tree:
9//! inserting or deleting nodes, relabeling anchors, setting or removing
10//! fields, moving subtrees, and manipulating hyper-edge fans.
11
12use std::collections::HashMap;
13
14use panproto_gat::Name;
15use panproto_schema::Edge;
16use serde::{Deserialize, Serialize};
17use smallvec::SmallVec;
18
19use crate::edit_error::EditError;
20use crate::fan::Fan;
21use crate::metadata::Node;
22use crate::value::Value;
23use crate::wtype::WInstance;
24
25/// A model of `ThEditableStructure` for W-type instances.
26///
27/// The edit monoid is free on these generators, quotiented by the
28/// equations `apply(identity(), s) = s` and
29/// `apply(compose(e1, e2), s) = apply(e2, apply(e1, s))`.
30#[derive(Clone, Debug, Serialize, Deserialize)]
31pub enum TreeEdit {
32    /// The monoid identity: no change.
33    Identity,
34
35    /// Insert a new child node under an existing parent.
36    InsertNode {
37        /// Parent node ID (must exist).
38        parent: u32,
39        /// ID for the new child node (must not already exist).
40        child_id: u32,
41        /// The new node data.
42        node: Node,
43        /// The edge connecting parent to child.
44        edge: Edge,
45    },
46
47    /// Delete a leaf node (a node with no children).
48    DeleteNode {
49        /// Node ID to delete.
50        id: u32,
51    },
52
53    /// Contract a non-leaf node: remove it and reattach its children
54    /// to its parent.
55    ContractNode {
56        /// Node ID to contract.
57        id: u32,
58    },
59
60    /// Change a node's anchor vertex.
61    RelabelNode {
62        /// Node ID to relabel.
63        id: u32,
64        /// The new anchor vertex.
65        new_anchor: Name,
66    },
67
68    /// Set or update a field in a node's `extra_fields`.
69    SetField {
70        /// Node ID.
71        node_id: u32,
72        /// Field name.
73        field: Name,
74        /// New value.
75        value: Value,
76    },
77
78    /// Remove a field from a node's `extra_fields`.
79    RemoveField {
80        /// Node ID.
81        node_id: u32,
82        /// Field name to remove.
83        field: Name,
84    },
85
86    /// Move a subtree to a new parent.
87    MoveSubtree {
88        /// Root of the subtree to move.
89        node_id: u32,
90        /// New parent node ID.
91        new_parent: u32,
92        /// Edge connecting new parent to the moved node.
93        edge: Edge,
94    },
95
96    /// Insert a new hyper-edge fan.
97    InsertFan {
98        /// The fan to insert.
99        fan: Fan,
100    },
101
102    /// Delete a hyper-edge fan by its hyper-edge ID.
103    DeleteFan {
104        /// The hyper-edge ID of the fan to remove.
105        hyper_edge_id: Name,
106    },
107
108    /// Merge multiple nodes into one via spatial join
109    /// (co-located annotations merge into a single node).
110    JoinFeatures {
111        /// The primary node that absorbs the others.
112        primary: u32,
113        /// Nodes to merge into the primary.
114        joined: Vec<u32>,
115        /// The resulting merged node data.
116        produce: Node,
117    },
118
119    /// A sequence of edits applied in order.
120    Sequence(Vec<Self>),
121}
122
123impl TreeEdit {
124    /// The monoid identity element.
125    #[must_use]
126    pub const fn identity() -> Self {
127        Self::Identity
128    }
129
130    /// Monoid multiplication: compose two edits into a sequence.
131    ///
132    /// Nested sequences are flattened and identity elements are elided.
133    #[must_use]
134    pub fn compose(self, other: Self) -> Self {
135        let mut steps = Vec::new();
136        flatten_into(&mut steps, self);
137        flatten_into(&mut steps, other);
138        match steps.len() {
139            0 => Self::Identity,
140            1 => steps.into_iter().next().unwrap_or(Self::Identity),
141            _ => Self::Sequence(steps),
142        }
143    }
144
145    /// Returns `true` if this edit is the identity (no-op).
146    #[must_use]
147    pub fn is_identity(&self) -> bool {
148        match self {
149            Self::Identity => true,
150            Self::Sequence(steps) => steps.iter().all(Self::is_identity),
151            _ => false,
152        }
153    }
154
155    /// Apply this edit to a W-type instance, mutating it in place.
156    ///
157    /// This is the partial monoid action `E × S → S`. The action is
158    /// partial because some edits fail on some states (e.g., deleting
159    /// a nonexistent node).
160    ///
161    /// # Errors
162    ///
163    /// Returns [`EditError`] if the edit cannot be applied to the
164    /// current instance state.
165    pub fn apply(&self, instance: &mut WInstance) -> Result<(), EditError> {
166        match self {
167            Self::Identity => Ok(()),
168
169            Self::InsertNode {
170                parent,
171                child_id,
172                node,
173                edge,
174            } => apply_insert_node(instance, *parent, *child_id, node, edge),
175
176            Self::DeleteNode { id } => apply_delete_node(instance, *id),
177
178            Self::ContractNode { id } => apply_contract_node(instance, *id),
179
180            Self::RelabelNode { id, new_anchor } => {
181                let n = instance
182                    .nodes
183                    .get_mut(id)
184                    .ok_or(EditError::NodeNotFound(*id))?;
185                n.anchor = new_anchor.clone();
186                Ok(())
187            }
188
189            Self::SetField {
190                node_id,
191                field,
192                value,
193            } => {
194                let n = instance
195                    .nodes
196                    .get_mut(node_id)
197                    .ok_or(EditError::NodeNotFound(*node_id))?;
198                n.extra_fields.insert(field.to_string(), value.clone());
199                Ok(())
200            }
201
202            Self::RemoveField { node_id, field } => {
203                let n = instance
204                    .nodes
205                    .get_mut(node_id)
206                    .ok_or(EditError::NodeNotFound(*node_id))?;
207                n.extra_fields.remove(field.as_ref());
208                Ok(())
209            }
210
211            Self::MoveSubtree {
212                node_id,
213                new_parent,
214                edge,
215            } => apply_move_subtree(instance, *node_id, *new_parent, edge),
216
217            Self::InsertFan { fan } => {
218                instance.fans.push(fan.clone());
219                Ok(())
220            }
221
222            Self::DeleteFan { hyper_edge_id } => {
223                let id_str = hyper_edge_id.as_ref();
224                let before = instance.fans.len();
225                instance.fans.retain(|f| f.hyper_edge_id != id_str);
226                if instance.fans.len() == before {
227                    return Err(EditError::FanNotFound(id_str.to_owned()));
228                }
229                Ok(())
230            }
231
232            Self::JoinFeatures {
233                primary,
234                joined,
235                produce,
236            } => apply_join_features(instance, *primary, joined, produce),
237
238            Self::Sequence(steps) => {
239                for step in steps {
240                    step.apply(instance)?;
241                }
242                Ok(())
243            }
244        }
245    }
246}
247
248// ---------------------------------------------------------------------------
249// Internal helpers
250// ---------------------------------------------------------------------------
251
252/// Flatten nested sequences and strip identities.
253fn flatten_into(out: &mut Vec<TreeEdit>, edit: TreeEdit) {
254    match edit {
255        TreeEdit::Identity => {}
256        TreeEdit::Sequence(steps) => {
257            for step in steps {
258                flatten_into(out, step);
259            }
260        }
261        other => out.push(other),
262    }
263}
264
265fn apply_insert_node(
266    instance: &mut WInstance,
267    parent: u32,
268    child_id: u32,
269    node: &Node,
270    edge: &Edge,
271) -> Result<(), EditError> {
272    if !instance.nodes.contains_key(&parent) {
273        return Err(EditError::ParentNotFound(parent));
274    }
275    if instance.nodes.contains_key(&child_id) {
276        return Err(EditError::DuplicateNodeId(child_id));
277    }
278    instance.nodes.insert(child_id, node.clone());
279    let arc = (parent, child_id, edge.clone());
280    instance.arcs.push(arc);
281    instance.parent_map.insert(child_id, parent);
282    instance
283        .children_map
284        .entry(parent)
285        .or_default()
286        .push(child_id);
287    Ok(())
288}
289
290fn apply_delete_node(instance: &mut WInstance, id: u32) -> Result<(), EditError> {
291    if !instance.nodes.contains_key(&id) {
292        return Err(EditError::NodeNotFound(id));
293    }
294    // Only allow deleting leaf nodes (no children).
295    let children = instance.children_map.get(&id);
296    if children.is_some_and(|c| !c.is_empty()) {
297        return Err(EditError::ApplyFailed(format!(
298            "cannot delete non-leaf node {id}; use ContractNode to remove and reattach children"
299        )));
300    }
301    instance.nodes.remove(&id);
302    // Remove arcs pointing to this node.
303    instance.arcs.retain(|&(_, child, _)| child != id);
304    // Remove from parent's children map.
305    if let Some(&parent_id) = instance.parent_map.get(&id) {
306        if let Some(siblings) = instance.children_map.get_mut(&parent_id) {
307            siblings.retain(|&c| c != id);
308        }
309    }
310    instance.parent_map.remove(&id);
311    instance.children_map.remove(&id);
312    // Remove any fans referencing this node.
313    instance
314        .fans
315        .retain(|f| f.parent != id && !f.children.values().any(|&c| c == id));
316    Ok(())
317}
318
319fn apply_contract_node(instance: &mut WInstance, id: u32) -> Result<(), EditError> {
320    if !instance.nodes.contains_key(&id) {
321        return Err(EditError::NodeNotFound(id));
322    }
323    if id == instance.root {
324        return Err(EditError::ApplyFailed(
325            "cannot contract the root node".to_owned(),
326        ));
327    }
328    let parent_id = instance
329        .parent_map
330        .get(&id)
331        .copied()
332        .ok_or_else(|| EditError::ApplyFailed(format!("node {id} has no parent")))?;
333
334    // Collect this node's children.
335    let children: SmallVec<u32, 4> = instance.children_map.get(&id).cloned().unwrap_or_default();
336
337    // Collect the original child edges before removal (for re-parenting).
338    let child_edges: HashMap<u32, Edge> = instance
339        .arcs
340        .iter()
341        .filter(|&&(p, _, _)| p == id)
342        .map(|&(_, c, ref e)| (c, e.clone()))
343        .collect();
344
345    // Find the edge from parent to this node.
346    let parent_edge: Option<Edge> = instance
347        .arcs
348        .iter()
349        .find(|&&(p, c, _)| p == parent_id && c == id)
350        .map(|(_, _, e)| e.clone());
351
352    // Remove arcs from parent to this node and from this node to children.
353    instance
354        .arcs
355        .retain(|&(p, c, _)| !((p == parent_id && c == id) || (p == id)));
356
357    // Reattach children to parent using the original child edge (with src
358    // updated to the parent's anchor) or the parent edge as fallback.
359    for &child_id in &children {
360        let edge = if let Some(mut child_edge) = child_edges.get(&child_id).cloned() {
361            // Use the original edge from contracted node to child, updating src.
362            if let Some(parent_node) = instance.nodes.get(&parent_id) {
363                child_edge.src.clone_from(&parent_node.anchor);
364            }
365            child_edge
366        } else if let Some(pe) = parent_edge.clone() {
367            // Fall back to the edge from parent to contracted node, updating tgt.
368            let mut e = pe;
369            if let Some(child_node) = instance.nodes.get(&child_id) {
370                e.tgt.clone_from(&child_node.anchor);
371            }
372            e
373        } else {
374            // Last resort: construct from actual node anchors.
375            let src = instance
376                .nodes
377                .get(&parent_id)
378                .map_or_else(|| Name::from(&*id.to_string()), |n| n.anchor.clone());
379            let tgt = instance
380                .nodes
381                .get(&child_id)
382                .map_or_else(|| Name::from(&*child_id.to_string()), |n| n.anchor.clone());
383            Edge {
384                src,
385                tgt,
386                kind: "prop".into(),
387                name: None,
388            }
389        };
390        instance.arcs.push((parent_id, child_id, edge));
391        instance.parent_map.insert(child_id, parent_id);
392        instance
393            .children_map
394            .entry(parent_id)
395            .or_default()
396            .push(child_id);
397    }
398
399    // Remove the contracted node.
400    instance.nodes.remove(&id);
401    instance.parent_map.remove(&id);
402    if let Some(siblings) = instance.children_map.get_mut(&parent_id) {
403        siblings.retain(|&c| c != id);
404    }
405    instance.children_map.remove(&id);
406
407    // Clean up fans referencing the contracted node.
408    instance
409        .fans
410        .retain(|f| f.parent != id && !f.children.values().any(|&c| c == id));
411
412    Ok(())
413}
414
415fn apply_move_subtree(
416    instance: &mut WInstance,
417    node_id: u32,
418    new_parent: u32,
419    edge: &Edge,
420) -> Result<(), EditError> {
421    if !instance.nodes.contains_key(&node_id) {
422        return Err(EditError::NodeNotFound(node_id));
423    }
424    if !instance.nodes.contains_key(&new_parent) {
425        return Err(EditError::ParentNotFound(new_parent));
426    }
427    // Prevent cycles: new_parent must not be a descendant of node_id.
428    if is_descendant(instance, new_parent, node_id) {
429        return Err(EditError::CycleDetected {
430            node_id,
431            new_parent,
432        });
433    }
434
435    // Remove old parent arc.
436    let old_parent = instance.parent_map.get(&node_id).copied();
437    instance.arcs.retain(|&(_, child, _)| child != node_id);
438    if let Some(old_p) = old_parent {
439        if let Some(siblings) = instance.children_map.get_mut(&old_p) {
440            siblings.retain(|&c| c != node_id);
441        }
442    }
443
444    // Insert new parent arc.
445    instance.arcs.push((new_parent, node_id, edge.clone()));
446    instance.parent_map.insert(node_id, new_parent);
447    instance
448        .children_map
449        .entry(new_parent)
450        .or_default()
451        .push(node_id);
452
453    Ok(())
454}
455
456/// Check if `candidate` is a descendant of `ancestor` in the instance tree.
457fn is_descendant(instance: &WInstance, candidate: u32, ancestor: u32) -> bool {
458    let mut current = candidate;
459    while let Some(&parent) = instance.parent_map.get(&current) {
460        if parent == ancestor {
461            return true;
462        }
463        current = parent;
464    }
465    false
466}
467
468fn apply_join_features(
469    instance: &mut WInstance,
470    primary: u32,
471    joined: &[u32],
472    produce: &Node,
473) -> Result<(), EditError> {
474    if !instance.nodes.contains_key(&primary) {
475        return Err(EditError::NodeNotFound(primary));
476    }
477    for &jid in joined {
478        if !instance.nodes.contains_key(&jid) {
479            return Err(EditError::NodeNotFound(jid));
480        }
481    }
482
483    // Replace the primary node with the merged produce.
484    instance.nodes.insert(primary, produce.clone());
485
486    // Delete the joined nodes (they are absorbed).
487    for &jid in joined {
488        // Remove arcs.
489        instance.arcs.retain(|&(_, child, _)| child != jid);
490        instance.arcs.retain(|&(parent, _, _)| parent != jid);
491        // Clean up maps.
492        if let Some(&parent_id) = instance.parent_map.get(&jid) {
493            if let Some(siblings) = instance.children_map.get_mut(&parent_id) {
494                siblings.retain(|&c| c != jid);
495            }
496        }
497        instance.nodes.remove(&jid);
498        instance.parent_map.remove(&jid);
499        instance.children_map.remove(&jid);
500    }
501
502    // Remove fans referencing joined nodes.
503    instance.fans.retain(|f| {
504        !joined.contains(&f.parent) && !f.children.values().any(|c| joined.contains(c))
505    });
506
507    Ok(())
508}
509
510#[cfg(test)]
511#[allow(clippy::unwrap_used, clippy::expect_used, clippy::redundant_clone)]
512mod tests {
513    use std::collections::HashMap;
514
515    use panproto_gat::Name;
516    use panproto_schema::Edge;
517
518    use crate::metadata::Node;
519    use crate::value::Value;
520    use crate::wtype::WInstance;
521
522    use super::TreeEdit;
523
524    fn sample_edge(src: &str, tgt: &str) -> Edge {
525        Edge {
526            src: src.into(),
527            tgt: tgt.into(),
528            kind: "prop".into(),
529            name: None,
530        }
531    }
532
533    fn two_node_instance() -> WInstance {
534        let mut nodes = HashMap::new();
535        nodes.insert(0, Node::new(0, "root"));
536        nodes.insert(1, Node::new(1, "child"));
537        let arcs = vec![(0, 1, sample_edge("root", "child"))];
538        WInstance::new(nodes, arcs, vec![], 0, Name::from("root"))
539    }
540
541    #[test]
542    fn identity_is_noop() {
543        let mut inst = two_node_instance();
544        let before = inst.nodes.len();
545        TreeEdit::identity().apply(&mut inst).unwrap();
546        assert_eq!(inst.nodes.len(), before);
547    }
548
549    #[test]
550    fn insert_then_delete_is_identity() {
551        let mut inst = two_node_instance();
552        let original_count = inst.nodes.len();
553        let node = Node::new(99, "new_child");
554        let edge = sample_edge("root", "new_child");
555
556        let insert = TreeEdit::InsertNode {
557            parent: 0,
558            child_id: 99,
559            node,
560            edge,
561        };
562        let delete = TreeEdit::DeleteNode { id: 99 };
563        let composed = insert.compose(delete);
564        composed.apply(&mut inst).unwrap();
565
566        assert_eq!(inst.nodes.len(), original_count);
567    }
568
569    #[test]
570    fn set_field_updates_value() {
571        let mut inst = two_node_instance();
572        let edit = TreeEdit::SetField {
573            node_id: 1,
574            field: Name::from("color"),
575            value: Value::Str("blue".into()),
576        };
577        edit.apply(&mut inst).unwrap();
578        assert_eq!(
579            inst.nodes[&1].extra_fields.get("color"),
580            Some(&Value::Str("blue".into()))
581        );
582    }
583
584    #[test]
585    fn remove_field() {
586        let mut inst = two_node_instance();
587        inst.nodes
588            .get_mut(&1)
589            .unwrap()
590            .extra_fields
591            .insert("color".into(), Value::Str("red".into()));
592
593        let edit = TreeEdit::RemoveField {
594            node_id: 1,
595            field: Name::from("color"),
596        };
597        edit.apply(&mut inst).unwrap();
598        assert!(!inst.nodes[&1].extra_fields.contains_key("color"));
599    }
600
601    #[test]
602    fn relabel_node() {
603        let mut inst = two_node_instance();
604        let edit = TreeEdit::RelabelNode {
605            id: 1,
606            new_anchor: Name::from("renamed_child"),
607        };
608        edit.apply(&mut inst).unwrap();
609        assert_eq!(inst.nodes[&1].anchor, Name::from("renamed_child"));
610    }
611
612    #[test]
613    fn move_subtree_reparents() {
614        let mut nodes = HashMap::new();
615        nodes.insert(0, Node::new(0, "root"));
616        nodes.insert(1, Node::new(1, "a"));
617        nodes.insert(2, Node::new(2, "b"));
618        let arcs = vec![
619            (0, 1, sample_edge("root", "a")),
620            (0, 2, sample_edge("root", "b")),
621        ];
622        let mut inst = WInstance::new(nodes, arcs, vec![], 0, Name::from("root"));
623
624        let edit = TreeEdit::MoveSubtree {
625            node_id: 2,
626            new_parent: 1,
627            edge: sample_edge("a", "b"),
628        };
629        edit.apply(&mut inst).unwrap();
630
631        assert_eq!(inst.parent_map[&2], 1);
632        assert!(inst.children_map[&1].contains(&2));
633    }
634
635    #[test]
636    fn contract_node_reattaches_children() {
637        let mut nodes = HashMap::new();
638        nodes.insert(0, Node::new(0, "root"));
639        nodes.insert(1, Node::new(1, "middle"));
640        nodes.insert(2, Node::new(2, "leaf"));
641        let arcs = vec![
642            (0, 1, sample_edge("root", "middle")),
643            (1, 2, sample_edge("middle", "leaf")),
644        ];
645        let mut inst = WInstance::new(nodes, arcs, vec![], 0, Name::from("root"));
646
647        let edit = TreeEdit::ContractNode { id: 1 };
648        edit.apply(&mut inst).unwrap();
649
650        assert!(!inst.nodes.contains_key(&1));
651        assert_eq!(inst.parent_map[&2], 0);
652        assert!(inst.children_map[&0].contains(&2));
653    }
654
655    #[test]
656    fn sequence_flattening() {
657        let e1 = TreeEdit::Identity;
658        let e2 = TreeEdit::Sequence(vec![TreeEdit::Identity, TreeEdit::Identity]);
659        let composed = e1.compose(e2);
660        assert!(composed.is_identity());
661    }
662
663    #[test]
664    fn monoid_associativity() {
665        let node_a = Node::new(10, "a");
666        let node_b = Node::new(11, "b");
667        let edge_a = sample_edge("root", "a");
668        let edge_b = sample_edge("root", "b");
669
670        let e1 = TreeEdit::InsertNode {
671            parent: 0,
672            child_id: 10,
673            node: node_a.clone(),
674            edge: edge_a.clone(),
675        };
676        let e2 = TreeEdit::InsertNode {
677            parent: 0,
678            child_id: 11,
679            node: node_b.clone(),
680            edge: edge_b.clone(),
681        };
682        let e3 = TreeEdit::DeleteNode { id: 10 };
683
684        // (e1 . e2) . e3
685        let left = e1.clone().compose(e2.clone()).compose(e3.clone());
686        // e1 . (e2 . e3)
687        let right = e1.compose(e2.compose(e3));
688
689        let mut inst_l = two_node_instance();
690        let mut inst_r = two_node_instance();
691        left.apply(&mut inst_l).unwrap();
692        right.apply(&mut inst_r).unwrap();
693
694        assert_eq!(inst_l.nodes.len(), inst_r.nodes.len());
695        for (id, node) in &inst_l.nodes {
696            let other = inst_r.nodes.get(id).expect("node should exist in both");
697            assert_eq!(node.anchor, other.anchor);
698        }
699    }
700
701    #[test]
702    fn monoid_identity_law() {
703        let edit = TreeEdit::SetField {
704            node_id: 1,
705            field: Name::from("x"),
706            value: Value::Int(42),
707        };
708
709        let mut inst1 = two_node_instance();
710        let mut inst2 = two_node_instance();
711
712        // apply(compose(identity, e), s) == apply(e, s)
713        TreeEdit::identity()
714            .compose(edit.clone())
715            .apply(&mut inst1)
716            .unwrap();
717        edit.apply(&mut inst2).unwrap();
718
719        assert_eq!(
720            inst1.nodes[&1].extra_fields.get("x"),
721            inst2.nodes[&1].extra_fields.get("x"),
722        );
723    }
724
725    #[test]
726    fn delete_nonexistent_node_fails() {
727        let mut inst = two_node_instance();
728        let err = TreeEdit::DeleteNode { id: 999 }.apply(&mut inst);
729        assert!(err.is_err());
730    }
731
732    #[test]
733    fn insert_duplicate_id_fails() {
734        let mut inst = two_node_instance();
735        let edit = TreeEdit::InsertNode {
736            parent: 0,
737            child_id: 1, // already exists
738            node: Node::new(1, "dup"),
739            edge: sample_edge("root", "dup"),
740        };
741        assert!(edit.apply(&mut inst).is_err());
742    }
743
744    #[test]
745    fn move_creates_cycle_fails() {
746        let mut nodes = HashMap::new();
747        nodes.insert(0, Node::new(0, "root"));
748        nodes.insert(1, Node::new(1, "child"));
749        nodes.insert(2, Node::new(2, "grandchild"));
750        let arcs = vec![
751            (0, 1, sample_edge("root", "child")),
752            (1, 2, sample_edge("child", "grandchild")),
753        ];
754        let mut inst = WInstance::new(nodes, arcs, vec![], 0, Name::from("root"));
755
756        // Moving node 0 (root) under node 2 (grandchild) would create a cycle,
757        // but root has no parent so MoveSubtree would try to reattach.
758        // Instead, move node 1 under node 2 (its own child).
759        let edit = TreeEdit::MoveSubtree {
760            node_id: 1,
761            new_parent: 2,
762            edge: sample_edge("grandchild", "child"),
763        };
764        assert!(edit.apply(&mut inst).is_err());
765    }
766}