fluidattacks-blends-domain 0.40.0

Blends functional core: pure AST graph to syntax graph (no_std)
Documentation
use alloc::collections::VecDeque;

use crate::stack::{StackGraph, StackNode};
use crate::NodeId;

/// The symbol stack a path carries through the graph, top first.
#[derive(Clone, PartialEq, Eq, Debug, Default)]
#[non_exhaustive]
pub struct StackState<'a> {
    /// Symbols still to resolve.
    pub symbols: VecDeque<&'a str>,
}

/// Why a node cannot be applied to a [`StackState`].
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub enum TransitionError {
    /// A pop found no symbol to consume.
    EmptySymbolStack,
    /// A pop found a different symbol on top.
    IncorrectPoppedSymbol,
    /// The node is not in the graph.
    UnknownNode,
}

impl<'a> StackState<'a> {
    /// An empty state.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Applies node `id` of `graph`.
    ///
    /// # Errors
    ///
    /// Returns the [`TransitionError`] that blocks the node, leaving the state unchanged.
    pub fn apply(&mut self, graph: &'a StackGraph, id: NodeId) -> Result<(), TransitionError> {
        match graph.nodes.get(&id).ok_or(TransitionError::UnknownNode)? {
            StackNode::PushSymbol(push) => self.symbols.push_front(&push.symbol),
            StackNode::PopSymbol(pop) => match self.symbols.front() {
                None => return Err(TransitionError::EmptySymbolStack),
                Some(top) if *top != pop.symbol => {
                    return Err(TransitionError::IncorrectPoppedSymbol)
                }
                Some(_) => {
                    self.symbols.pop_front();
                }
            },
            StackNode::Root | StackNode::Scope(_) => {}
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::{StackState, TransitionError};
    use crate::stack::{Definition, StackGraph, StackNode, StackScope, SymbolPop, SymbolPush};
    use crate::NodeId;
    use alloc::borrow::ToOwned;
    use alloc::collections::VecDeque;
    use alloc::vec::Vec;

    fn push(symbol: &str) -> StackNode {
        StackNode::PushSymbol(SymbolPush {
            symbol: symbol.to_owned(),
            precedence: 0,
            is_reference: true,
        })
    }

    fn pop(symbol: &str) -> StackNode {
        StackNode::PopSymbol(SymbolPop {
            symbol: symbol.to_owned(),
            precedence: 0,
            definition: Some(Definition::default()),
            import: None,
        })
    }

    fn graph(nodes: &[StackNode]) -> StackGraph {
        let mut graph = StackGraph::new();
        for (id, node) in (10..).zip(nodes) {
            graph.add_node(NodeId(id), node.clone());
        }
        graph
    }

    fn run<'a>(graph: &'a StackGraph, state: &mut StackState<'a>) -> Result<(), TransitionError> {
        graph
            .nodes
            .keys()
            .try_for_each(|id| state.apply(graph, *id))
    }

    fn apply_last(nodes: &[StackNode]) -> (Result<(), TransitionError>, bool) {
        let graph = graph(nodes);
        let ids: Vec<NodeId> = graph.nodes.keys().copied().collect();
        let (last, setup) = ids.split_last().unwrap();
        let mut state = StackState::new();
        for id in setup {
            state.apply(&graph, *id).unwrap();
        }
        let before = state.clone();
        let result = state.apply(&graph, *last);
        (result, state == before)
    }

    #[test]
    fn pushes_stack_symbols_with_the_latest_on_top() {
        let graph = graph(&[push("b"), push("a")]);
        let mut state = StackState::new();
        run(&graph, &mut state).unwrap();

        assert_eq!(state.symbols, VecDeque::from(["a", "b"]));
    }

    #[test]
    fn a_pop_consumes_the_matching_top_symbol() {
        let graph = graph(&[push("b"), push("a"), pop("a")]);
        let mut state = StackState::new();
        run(&graph, &mut state).unwrap();

        assert_eq!(state.symbols, VecDeque::from(["b"]));
    }

    #[test]
    fn a_pop_on_an_empty_stack_fails_without_changing_the_state() {
        assert_eq!(
            apply_last(&[pop("a")]),
            (Err(TransitionError::EmptySymbolStack), true)
        );
    }

    #[test]
    fn a_pop_of_another_symbol_fails_without_changing_the_state() {
        assert_eq!(
            apply_last(&[push("b"), pop("a")]),
            (Err(TransitionError::IncorrectPoppedSymbol), true)
        );
    }

    #[test]
    fn roots_and_scopes_leave_the_state_unchanged() {
        let graph = graph(&[
            push("a"),
            StackNode::Root,
            StackNode::Scope(StackScope::default()),
        ]);
        let mut state = StackState::new();
        run(&graph, &mut state).unwrap();

        assert_eq!(state.symbols, VecDeque::from(["a"]));
    }

    #[test]
    fn a_missing_node_cannot_be_applied() {
        let graph = graph(&[]);

        assert_eq!(
            StackState::new().apply(&graph, NodeId(99)),
            Err(TransitionError::UnknownNode)
        );
    }
}