use alloc::vec::Vec;
use crate::stack::resolve::{
PartialBindings, PartialSymbolStack, SymbolStackVariable, TransitionError, UnifyError,
};
use crate::stack::{StackGraph, StackNode};
use crate::NodeId;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub struct PathEdge {
pub source: NodeId,
pub precedence: i64,
}
#[derive(Clone, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub struct PartialPath<'a> {
pub start: NodeId,
pub end: NodeId,
pub pre: PartialSymbolStack<'a>,
pub post: PartialSymbolStack<'a>,
pub edges: Vec<PathEdge>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub enum PathError {
DisjointPaths,
Transition(TransitionError),
Unify(UnifyError),
UnboundVariable,
VariableOverflow,
}
impl From<TransitionError> for PathError {
fn from(error: TransitionError) -> Self {
Self::Transition(error)
}
}
impl From<UnifyError> for PathError {
fn from(error: UnifyError) -> Self {
Self::Unify(error)
}
}
impl PartialPath<'_> {
pub fn concatenate(&self, graph: &StackGraph, rhs: &Self) -> Result<Self, PathError> {
if self.end != rhs.start {
return Err(PathError::DisjointPaths);
}
let largest = self
.pre
.variable
.max(self.post.variable)
.map_or(0, SymbolStackVariable::get);
let rhs_pre = rhs
.pre
.with_offset(largest)
.ok_or(PathError::VariableOverflow)?;
let rhs_post = rhs
.post
.with_offset(largest)
.ok_or(PathError::VariableOverflow)?;
let (left, right) = match graph
.nodes
.get(&self.end)
.ok_or(TransitionError::UnknownNode)?
{
StackNode::PushSymbol(push) => (pop_top(&self.post, &push.symbol)?, rhs_pre),
StackNode::PopSymbol(pop) => (self.post.clone(), pop_top(&rhs_pre, &pop.symbol)?),
StackNode::Root | StackNode::Scope(_) => (self.post.clone(), rhs_pre),
};
let mut bindings = PartialBindings::new();
bindings.unify(&left, &right)?;
let pre = self.pre.apply_bindings(&bindings);
let post = rhs_post.apply_bindings(&bindings);
if post.variable.is_some() && post.variable != pre.variable {
return Err(PathError::UnboundVariable);
}
Ok(Self {
start: self.start,
end: rhs.end,
pre,
post,
edges: self.edges.iter().chain(&rhs.edges).copied().collect(),
})
}
}
fn pop_top<'a>(
symbols: &PartialSymbolStack<'a>,
symbol: &str,
) -> Result<PartialSymbolStack<'a>, TransitionError> {
let mut rest = symbols.clone();
match rest.symbols.pop_front() {
None => Err(TransitionError::EmptySymbolStack),
Some(top) if top != symbol => Err(TransitionError::IncorrectPoppedSymbol),
Some(_) => Ok(rest),
}
}
#[cfg(test)]
mod tests {
use super::{PartialPath, PathEdge, PathError};
use crate::stack::resolve::{
PartialSymbolStack, SymbolStackVariable, TransitionError, UnifyError,
};
use crate::stack::{Definition, StackGraph, StackNode, StackScope, SymbolPop, SymbolPush};
use crate::NodeId;
use alloc::borrow::ToOwned;
use alloc::vec::Vec;
const PUSH_X: NodeId = NodeId(10);
const PUSH_Y: NodeId = NodeId(11);
const SCOPE: NodeId = NodeId(20);
const OTHER_SCOPE: NodeId = NodeId(21);
const POP_X: NodeId = NodeId(30);
fn push(symbol: &str) -> StackNode {
StackNode::PushSymbol(SymbolPush {
symbol: symbol.to_owned(),
precedence: 0,
is_reference: true,
})
}
fn graph() -> StackGraph {
let mut graph = StackGraph::new();
graph.add_node(PUSH_X, push("x"));
graph.add_node(PUSH_Y, push("y"));
graph.add_node(SCOPE, StackNode::Scope(StackScope::default()));
graph.add_node(OTHER_SCOPE, StackNode::Scope(StackScope::default()));
graph.add_node(
POP_X,
StackNode::PopSymbol(SymbolPop {
symbol: "x".to_owned(),
precedence: 0,
definition: Some(Definition::default()),
import: None,
}),
);
graph
}
fn symbols<'a>(names: &[&'a str], variable: Option<u32>) -> PartialSymbolStack<'a> {
PartialSymbolStack::new(
names.iter().copied().collect(),
variable.and_then(SymbolStackVariable::new),
)
}
fn path<'a>(
start: NodeId,
end: NodeId,
pre: PartialSymbolStack<'a>,
post: PartialSymbolStack<'a>,
) -> PartialPath<'a> {
PartialPath {
start,
end,
pre,
post,
edges: Vec::new(),
}
}
fn through(start: NodeId, end: NodeId) -> PartialPath<'static> {
path(start, end, symbols(&[], Some(1)), symbols(&[], Some(1)))
}
#[test]
fn a_reference_path_joined_with_a_definition_path_leaves_the_stack_as_it_was() {
let graph = graph();
let reference = path(
PUSH_X,
SCOPE,
symbols(&[], Some(1)),
symbols(&["x"], Some(1)),
);
let definition = path(
SCOPE,
POP_X,
symbols(&["x"], Some(1)),
symbols(&[], Some(1)),
);
assert_eq!(
reference.concatenate(&graph, &definition),
Ok(through(PUSH_X, POP_X))
);
}
#[test]
fn paths_that_do_not_meet_do_not_concatenate() {
assert_eq!(
through(PUSH_X, SCOPE).concatenate(&graph(), &through(OTHER_SCOPE, POP_X)),
Err(PathError::DisjointPaths)
);
}
#[test]
fn a_push_where_the_paths_meet_is_counted_once() {
let graph = graph();
let lhs = path(
SCOPE,
PUSH_X,
symbols(&[], Some(1)),
symbols(&["x"], Some(1)),
);
let rhs = path(
PUSH_X,
PUSH_Y,
symbols(&[], Some(1)),
symbols(&["y", "x"], Some(1)),
);
assert_eq!(
lhs.concatenate(&graph, &rhs).map(|joined| joined.post),
Ok(symbols(&["y", "x"], Some(1)))
);
}
#[test]
fn a_pop_where_the_paths_meet_is_required_once() {
let graph = graph();
let lhs = path(
SCOPE,
POP_X,
symbols(&["x"], Some(1)),
symbols(&[], Some(1)),
);
let rhs = path(
POP_X,
OTHER_SCOPE,
symbols(&["x"], Some(1)),
symbols(&[], Some(1)),
);
assert_eq!(
lhs.concatenate(&graph, &rhs).map(|joined| joined.pre),
Ok(symbols(&["x"], Some(1)))
);
}
#[test]
fn a_push_of_another_symbol_where_the_paths_meet_does_not_concatenate() {
let lhs = path(
SCOPE,
PUSH_X,
symbols(&[], Some(1)),
symbols(&["y"], Some(1)),
);
assert_eq!(
lhs.concatenate(&graph(), &through(PUSH_X, PUSH_Y)),
Err(PathError::Transition(
TransitionError::IncorrectPoppedSymbol
))
);
}
#[test]
fn a_symbol_left_that_the_next_path_does_not_need_does_not_concatenate() {
let lhs = path(
PUSH_X,
SCOPE,
symbols(&[], Some(1)),
symbols(&["x"], Some(1)),
);
let rhs = path(
SCOPE,
OTHER_SCOPE,
symbols(&["y"], Some(1)),
symbols(&[], Some(1)),
);
assert_eq!(
lhs.concatenate(&graph(), &rhs),
Err(PathError::Unify(UnifyError))
);
}
#[test]
fn the_variables_of_the_second_path_are_renamed_apart_from_the_first() {
let graph = graph();
let lhs = path(
PUSH_X,
SCOPE,
symbols(&[], Some(1)),
symbols(&["x"], Some(1)),
);
let rhs = path(
SCOPE,
OTHER_SCOPE,
symbols(&[], Some(1)),
symbols(&["y"], Some(1)),
);
assert_eq!(
lhs.concatenate(&graph, &rhs).map(|joined| joined.post),
Ok(symbols(&["y", "x"], Some(1)))
);
}
#[test]
fn renaming_past_the_last_variable_number_does_not_concatenate() {
let rhs = path(
SCOPE,
OTHER_SCOPE,
symbols(&[], Some(u32::MAX)),
symbols(&[], Some(u32::MAX)),
);
assert_eq!(
through(PUSH_X, SCOPE).concatenate(&graph(), &rhs),
Err(PathError::VariableOverflow)
);
}
#[test]
fn a_variable_left_that_was_not_needed_does_not_concatenate() {
let lhs = path(PUSH_X, SCOPE, symbols(&[], None), symbols(&[], Some(1)));
assert_eq!(
lhs.concatenate(&graph(), &through(SCOPE, OTHER_SCOPE)),
Err(PathError::UnboundVariable)
);
}
#[test]
fn the_joined_path_follows_the_edges_of_both_in_order() {
let edge = |source| PathEdge {
source,
precedence: 0,
};
let lhs = PartialPath {
edges: Vec::from([edge(PUSH_X)]),
..through(PUSH_X, SCOPE)
};
let rhs = PartialPath {
edges: Vec::from([edge(SCOPE)]),
..through(SCOPE, OTHER_SCOPE)
};
assert_eq!(
lhs.concatenate(&graph(), &rhs).map(|joined| joined.edges),
Ok(Vec::from([edge(PUSH_X), edge(SCOPE)]))
);
}
}