use crate::error::Error;
use crate::graph::{Graph, Node, NodeId};
use crate::parse::Position;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DiffNode {
Leaf {
axis: u8,
minuend: usize,
subtrahend: usize,
},
Neg(NodeId),
Add(NodeId, NodeId),
Sub(NodeId, NodeId),
Mul(NodeId, NodeId),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DiffGraph {
pub nodes: Vec<DiffNode>,
pub root: NodeId,
pub degrees: Vec<u32>,
}
#[derive(Clone, Copy)]
enum Mapped {
Coord { point: usize, axis: u8 },
Literal(i64),
Node(NodeId),
}
pub fn difference_graph(graph: &Graph, file: &str, position: Position) -> Result<DiffGraph, Error> {
let fail = |message: String| Error {
file: file.to_string(),
line: position.line,
column: position.column,
message,
};
let mut out = DiffGraph {
nodes: Vec::new(),
root: 0,
degrees: Vec::new(),
};
let mut mapped: Vec<Mapped> = Vec::with_capacity(graph.nodes.len());
for node in &graph.nodes {
let value = match *node {
Node::Coord { point, axis } => Mapped::Coord { point, axis },
Node::Int(n) => Mapped::Literal(n),
Node::Sub(x, y) => match (mapped[x], mapped[y]) {
(Mapped::Coord { point: p, axis: i }, Mapped::Coord { point: q, axis: j }) => {
if i != j {
return Err(fail(format!(
"a difference of coordinates must use one axis, found {} and {}",
coordinate(p, i),
coordinate(q, j)
)));
}
if p == q {
return Err(fail(format!(
"a difference of {} with itself is always zero",
coordinate(p, i)
)));
}
Mapped::Node(push(
&mut out,
DiffNode::Leaf {
axis: i,
minuend: p,
subtrahend: q,
},
1,
))
}
(Mapped::Literal(0), Mapped::Node(id)) => {
let degree = out.degrees[id];
Mapped::Node(push(&mut out, DiffNode::Neg(id), degree))
}
(Mapped::Literal(0), operand) => return Err(fail(operand_problem(operand))),
(x, y) => combine(&mut out, DiffNode::Sub, x, y, &fail)?,
},
Node::Add(x, y) => combine(&mut out, DiffNode::Add, mapped[x], mapped[y], &fail)?,
Node::Mul(x, y) => combine(&mut out, DiffNode::Mul, mapped[x], mapped[y], &fail)?,
};
mapped.push(value);
}
match mapped[graph.root] {
Mapped::Node(root) => {
out.root = root;
Ok(out)
}
other => Err(fail(operand_problem(other))),
}
}
fn combine(
out: &mut DiffGraph,
make: fn(NodeId, NodeId) -> DiffNode,
x: Mapped,
y: Mapped,
fail: &dyn Fn(String) -> Error,
) -> Result<Mapped, Error> {
let (Mapped::Node(x), Mapped::Node(y)) = (x, y) else {
let bad = if matches!(x, Mapped::Node(_)) { y } else { x };
return Err(fail(operand_problem(bad)));
};
let (gx, gy) = (out.degrees[x], out.degrees[y]);
let node = make(x, y);
let degree = if matches!(node, DiffNode::Mul(..)) {
gx.checked_add(gy).ok_or_else(|| {
fail(format!(
"a product has a degree in the differences above {}",
u32::MAX
))
})?
} else if gx == gy {
gx
} else {
return Err(fail(format!(
"a sum or difference combines terms of degree {gx} and {gy} in the differences"
)));
};
Ok(Mapped::Node(push(out, node, degree)))
}
fn push(out: &mut DiffGraph, node: DiffNode, degree: u32) -> NodeId {
out.nodes.push(node);
out.degrees.push(degree);
out.nodes.len() - 1
}
fn operand_problem(bad: Mapped) -> String {
match bad {
Mapped::Coord { point, axis } => format!(
"the coordinate {} is used outside a difference of coordinates",
coordinate(point, axis)
),
Mapped::Literal(n) => format!(
"the integer literal {n} is not allowed in the main form, \
except as the zero of a negation"
),
Mapped::Node(_) => unreachable!("only coordinates and literals are problems"),
}
}
fn coordinate(point: usize, axis: u8) -> String {
format!("`point {point}`.{}", ["x", "y", "z"][usize::from(axis)])
}