openquiver-graph 0.1.0

OpenQuiver graph IR: nodes, pins, types. Bottom layer, no internal dependencies.
Documentation
//! Editing a graph together with everything keyed by the identities in it.

use std::collections::BTreeSet;

use crate::{Anchor, Edge, Graph, Layout, Node, NodeName, PinRef, Trivia};

/// Why a rename could not be performed.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum RenameError {
    #[error("there is no node named `{0}`")]
    NoSuchNode(String),
    #[error("a node named `{0}` already exists")]
    NameTaken(String),
    #[error("`{0}` is not a name the format can write")]
    NotAnIdentifier(String),
}

/// A graph together with everything keyed by the identities inside it.
///
/// The three tables have to move as one. They are keyed by the same node names
/// and wires, so an edit that updated only some of them would leave the rest
/// pointing at things that no longer exist — which, for the comment table,
/// means deleting somebody's writing on the next save.
///
/// Source spans deliberately do **not** live here. A span describes the text a
/// file was parsed from, and the first edit makes every one of them stale, not
/// merely the ones naming the edited node. Keeping them out means a stale span
/// cannot be read by accident.
#[derive(Debug, Clone, PartialEq)]
pub struct Document {
    pub graph: Graph,
    pub trivia: Trivia,
    pub layout: Layout,
}

impl Document {
    pub fn new(graph: Graph, trivia: Trivia, layout: Layout) -> Self {
        Self {
            graph,
            trivia,
            layout,
        }
    }

    /// Removes a node together with every wire that touches it, its position,
    /// and the comments belonging to all of them.
    ///
    /// Wires are not left dangling: a dangling wire is a state the compiler
    /// would have to consider on every check, and a later node reusing the freed
    /// name would silently inherit both the wires and the dead node's comments.
    pub fn remove_node(&mut self, name: &str) -> Option<Node> {
        let key = NodeName::new(name);
        let node = self.graph.nodes.remove(&key)?;

        // Compare against the normalized key, not the caller's argument: a
        // decomposed name would otherwise match the map but not the wires, and
        // leave exactly the dangling state this function exists to prevent.
        let name = key.as_str();
        let touching: Vec<Edge> = self
            .graph
            .edges
            .iter()
            .filter(|e| e.from().node() == name || e.to().node() == name)
            .cloned()
            .collect();
        for edge in touching {
            self.graph.edges.remove(&edge);
            self.trivia.0.remove(&Anchor::Edge(edge));
        }

        self.trivia
            .0
            .retain(|anchor, _| anchor.node() != Some(&key));
        self.layout.0.remove(&key);

        Some(node)
    }

    /// Removes one wire and the comments attached to it.
    pub fn remove_edge(&mut self, edge: &Edge) -> bool {
        self.trivia.0.remove(&Anchor::Edge(edge.clone()));
        self.graph.edges.remove(edge)
    }

    /// Renames a node, rewriting every wire that mentions it and carrying its
    /// comments and its position across.
    ///
    /// This is the price of letting a node's name be its identity, and it is
    /// paid here rather than by each caller so that it is paid the same way
    /// every time.
    pub fn rename_node(&mut self, from: &str, to: &str) -> Result<(), RenameError> {
        let old = NodeName::new(from);
        let new = NodeName::new(to);

        if !self.graph.nodes.contains_key(&old) {
            return Err(RenameError::NoSuchNode(from.to_owned()));
        }
        // A name the grammar cannot write would make the file unparseable the
        // moment it is saved, so it is refused here rather than discovered then.
        if !is_identifier(new.as_str()) {
            return Err(RenameError::NotAnIdentifier(to.to_owned()));
        }
        // Merging two nodes into one identity is the duplicate-name failure by
        // another route, so it is refused rather than resolved.
        if old != new && self.graph.nodes.contains_key(&new) {
            return Err(RenameError::NameTaken(to.to_owned()));
        }
        if old == new {
            return Ok(());
        }

        let mut node = self.graph.nodes.remove(&old).expect("checked above");
        node.name = new.clone();
        self.graph.nodes.insert(new.clone(), node);

        let rewrite = |pin: &PinRef| -> PinRef {
            if pin.node == old {
                PinRef::new(new.clone(), pin.pin.clone())
            } else {
                pin.clone()
            }
        };

        let mut moved = BTreeSet::new();
        for edge in std::mem::take(&mut self.graph.edges) {
            let next = Edge::new(edge.kind, rewrite(edge.from()), rewrite(edge.to()));
            // Two wires can collapse into one here, so comments are appended
            // rather than replaced.
            if let Some(comments) = self.trivia.0.remove(&Anchor::Edge(edge)) {
                self.trivia
                    .0
                    .entry(Anchor::Edge(next.clone()))
                    .or_default()
                    .extend(comments);
            }
            moved.insert(next);
        }
        self.graph.edges = moved;

        let rekeyed: Vec<(Anchor, Vec<String>)> = self
            .trivia
            .0
            .extract_if(.., |anchor, _| anchor.node() == Some(&old))
            .map(|(anchor, comments)| (anchor.with_node(new.clone()), comments))
            .collect();
        for (anchor, comments) in rekeyed {
            self.trivia.0.entry(anchor).or_default().extend(comments);
        }

        if let Some(at) = self.layout.0.remove(&old) {
            self.layout.0.insert(new, at);
        }

        Ok(())
    }
}

/// Whether a name is one the grammar can write back out.
///
/// Unicode UAX #31, the rule Rust applies to its own identifiers.
pub fn is_identifier(name: &str) -> bool {
    let mut chars = name.chars();
    let Some(first) = chars.next() else {
        return false;
    };
    (first == '_' || unicode_ident::is_xid_start(first))
        && chars.all(unicode_ident::is_xid_continue)
}