use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write as _;
use super::{err, escape, Parser, SubstitutionQueryError, TWO_SIDES};
use crate::link_store::DoubletLink;
use crate::normal_markov::RewriteHalt;
use crate::substitution::CrudEvent;
const LINK_SLOTS: &str = "a link is written (source target) or (index: source target), \
with each slot a value or a `$` variable";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Slot {
Value(String),
Variable(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinkPattern {
pub index: Option<Slot>,
pub source: Slot,
pub target: Slot,
}
impl LinkPattern {
fn slots(&self) -> impl Iterator<Item = &Slot> {
self.index.iter().chain([&self.source, &self.target])
}
fn variables(&self) -> BTreeSet<&str> {
self.slots()
.filter_map(|slot| match slot {
Slot::Variable(name) => Some(name.as_str()),
Slot::Value(_) => None,
})
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinkRewriteRule {
pub pattern: Option<LinkPattern>,
pub replacement: Option<LinkPattern>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinkRewriteStep {
pub rule_index: usize,
pub effect: CrudEvent,
pub link: DoubletLink,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinkRewriteOutcome {
pub links: Vec<DoubletLink>,
pub trace: Vec<LinkRewriteStep>,
pub halt: RewriteHalt,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinkRewriteProgram {
pub rules: Vec<LinkRewriteRule>,
pub max_steps: usize,
}
impl LinkRewriteProgram {
#[must_use]
pub const fn new(rules: Vec<LinkRewriteRule>, max_steps: usize) -> Self {
Self { rules, max_steps }
}
#[must_use]
pub fn matched_links(&self, links: &[DoubletLink]) -> Vec<DoubletLink> {
links
.iter()
.filter(|link| {
self.rules.iter().any(|rule| {
rule.pattern
.as_ref()
.is_some_and(|pattern| match_link(pattern, link).is_some())
})
})
.cloned()
.collect()
}
#[must_use]
pub fn execute(&self, links: &[DoubletLink]) -> LinkRewriteOutcome {
let mut store = links.to_vec();
let mut trace = Vec::new();
for _ in 0..self.max_steps {
let Some((rule_index, transition)) = self.next_transition(&store) else {
return LinkRewriteOutcome {
links: store,
trace,
halt: RewriteHalt::NoApplicableRule,
};
};
let link = apply(&mut store, transition);
trace.push(LinkRewriteStep {
rule_index,
effect: link_substitution_effect(&self.rules[rule_index]),
link,
});
}
LinkRewriteOutcome {
links: store,
trace,
halt: RewriteHalt::StepLimit,
}
}
fn next_transition(&self, store: &[DoubletLink]) -> Option<(usize, Transition)> {
self.rules
.iter()
.enumerate()
.find_map(|(index, rule)| rule_transition(rule, store).map(|found| (index, found)))
}
}
#[must_use]
pub fn link_substitution_effect(rule: &LinkRewriteRule) -> CrudEvent {
match (&rule.pattern, &rule.replacement) {
(None, None) => CrudEvent::Manual,
(None, Some(_)) => CrudEvent::Create,
(Some(_), None) => CrudEvent::Delete,
(Some(pattern), Some(replacement)) => {
if pattern == replacement {
CrudEvent::Read
} else {
CrudEvent::Update
}
}
}
}
pub fn parse_link_substitution_query(
query: &str,
max_steps: usize,
) -> Result<LinkRewriteProgram, SubstitutionQueryError> {
let mut parser = Parser::new(query);
let matching = parser.parse_link_side()?;
parser.skip_whitespace();
if parser.at_end() {
return Err(err(format!("{TWO_SIDES}; found only one")));
}
let substituting = parser.parse_link_side()?;
parser.skip_whitespace();
if !parser.at_end() {
return Err(err(format!("{TWO_SIDES}; found trailing input")));
}
Ok(LinkRewriteProgram::new(
pair_links(matching, substituting)?,
max_steps,
))
}
#[must_use]
pub fn render_link_substitution_query(program: &LinkRewriteProgram) -> String {
let rules = &program.rules;
let patterns_empty = rules.iter().all(|rule| rule.pattern.is_none());
let replacements_empty = rules.iter().all(|rule| rule.replacement.is_none());
let elide_matching = !rules.is_empty() && patterns_empty && !replacements_empty;
let elide_substituting = !rules.is_empty() && replacements_empty && !patterns_empty;
let matching = if elide_matching {
String::from("()")
} else {
render_link_side(rules.iter().map(|rule| rule.pattern.as_ref()))
};
let substituting = if elide_substituting {
String::from("()")
} else {
render_link_side(rules.iter().map(|rule| rule.replacement.as_ref()))
};
format!("{matching} {substituting}")
}
#[must_use]
pub fn render_link(link: &DoubletLink) -> String {
let pattern = LinkPattern {
index: Some(Slot::Value(link.index.clone())),
source: Slot::Value(link.from.clone()),
target: Slot::Value(link.to.clone()),
};
let rendered = render_link_side([Some(&pattern)].into_iter());
rendered[1..rendered.len() - 1].to_owned()
}
fn render_link_side<'a>(patterns: impl Iterator<Item = Option<&'a LinkPattern>>) -> String {
let mut rendered = String::from("(");
for (position, pattern) in patterns.enumerate() {
if position > 0 {
rendered.push(' ');
}
match pattern {
None => rendered.push_str("()"),
Some(pattern) => {
rendered.push('(');
if let Some(index) = &pattern.index {
let _ = write!(rendered, "{}: ", render_slot(index));
}
let _ = write!(
rendered,
"{} {})",
render_slot(&pattern.source),
render_slot(&pattern.target)
);
}
}
}
rendered.push(')');
rendered
}
fn render_slot(slot: &Slot) -> String {
match slot {
Slot::Variable(name) => format!("${name}"),
Slot::Value(value) => {
if !value.is_empty() && value.chars().all(is_token_char) {
value.clone()
} else {
format!("\"{}\"", escape(value))
}
}
}
}
fn pair_links(
matching: Vec<Option<LinkPattern>>,
substituting: Vec<Option<LinkPattern>>,
) -> Result<Vec<LinkRewriteRule>, SubstitutionQueryError> {
let rules: Vec<LinkRewriteRule> = match (matching.is_empty(), substituting.is_empty()) {
(true, true) => Vec::new(),
(true, false) => substituting
.into_iter()
.map(|replacement| LinkRewriteRule {
pattern: None,
replacement,
})
.collect(),
(false, true) => matching
.into_iter()
.map(|pattern| LinkRewriteRule {
pattern,
replacement: None,
})
.collect(),
(false, false) => {
if matching.len() != substituting.len() {
return Err(err(format!(
"operand count must match across sides, or one side must be `()`; \
the matching side has {} and the substitution side has {}",
matching.len(),
substituting.len()
)));
}
matching
.into_iter()
.zip(substituting)
.map(|(pattern, replacement)| LinkRewriteRule {
pattern,
replacement,
})
.collect()
}
};
for rule in &rules {
validate_link_rule(rule)?;
}
Ok(rules)
}
fn validate_link_rule(rule: &LinkRewriteRule) -> Result<(), SubstitutionQueryError> {
let Some(replacement) = &rule.replacement else {
return Ok(());
};
let bound = rule
.pattern
.as_ref()
.map(LinkPattern::variables)
.unwrap_or_default();
if let Some(unbound) = replacement
.variables()
.into_iter()
.find(|name| !bound.contains(name))
{
return Err(err(format!(
"`${unbound}` is used on the substitution side but never bound on the matching side"
)));
}
if rule.pattern.is_none() && replacement.index.is_some() {
return Err(err(
"creation cannot force an index: write `() ((source target))` \
and let the store assign one",
));
}
Ok(())
}
fn rule_transition(rule: &LinkRewriteRule, store: &[DoubletLink]) -> Option<Transition> {
match (&rule.pattern, &rule.replacement) {
(None, None) => None,
(None, Some(replacement)) => {
let empty = BTreeMap::new();
let link = DoubletLink {
index: allocate_index(store),
from: resolve(&replacement.source, &empty),
to: resolve(&replacement.target, &empty),
};
(!store
.iter()
.any(|held| held.from == link.from && held.to == link.to))
.then_some(Transition::Insert(link))
}
(Some(pattern), None) => store
.iter()
.position(|link| match_link(pattern, link).is_some())
.map(Transition::Remove),
(Some(pattern), Some(replacement)) => {
store.iter().enumerate().find_map(|(position, link)| {
let bindings = match_link(pattern, link)?;
let next = substitute(replacement, &bindings, link);
(next != *link).then_some(Transition::Replace(position, next))
})
}
}
}
enum Transition {
Insert(DoubletLink),
Remove(usize),
Replace(usize, DoubletLink),
}
fn apply(store: &mut Vec<DoubletLink>, transition: Transition) -> DoubletLink {
match transition {
Transition::Insert(link) => {
store.push(link.clone());
link
}
Transition::Remove(position) => store.remove(position),
Transition::Replace(position, link) => {
store.remove(position);
if !store.contains(&link) {
store.insert(position, link.clone());
}
link
}
}
}
fn match_link(pattern: &LinkPattern, link: &DoubletLink) -> Option<BTreeMap<String, String>> {
let mut bindings = BTreeMap::new();
if let Some(index) = &pattern.index {
bind(index, &link.index, &mut bindings)?;
}
bind(&pattern.source, &link.from, &mut bindings)?;
bind(&pattern.target, &link.to, &mut bindings)?;
Some(bindings)
}
fn bind(slot: &Slot, value: &str, bindings: &mut BTreeMap<String, String>) -> Option<()> {
match slot {
Slot::Value(expected) => (expected == value).then_some(()),
Slot::Variable(name) => {
let bound = bindings
.entry(name.clone())
.or_insert_with(|| value.to_owned());
(bound == value).then_some(())
}
}
}
fn substitute(
replacement: &LinkPattern,
bindings: &BTreeMap<String, String>,
matched: &DoubletLink,
) -> DoubletLink {
DoubletLink {
index: replacement
.index
.as_ref()
.map_or_else(|| matched.index.clone(), |slot| resolve(slot, bindings)),
from: resolve(&replacement.source, bindings),
to: resolve(&replacement.target, bindings),
}
}
fn resolve(slot: &Slot, bindings: &BTreeMap<String, String>) -> String {
match slot {
Slot::Value(value) => value.clone(),
Slot::Variable(name) => bindings
.get(name)
.cloned()
.unwrap_or_else(|| format!("${name}")),
}
}
fn allocate_index(store: &[DoubletLink]) -> String {
store
.iter()
.filter_map(|link| link.index.parse::<u64>().ok())
.max()
.map_or(1, |highest| highest.saturating_add(1))
.to_string()
}
fn is_token_char(character: char) -> bool {
character.is_alphanumeric() || matches!(character, '_' | '-' | '.')
}
impl Parser<'_> {
fn parse_link_side(&mut self) -> Result<Vec<Option<LinkPattern>>, SubstitutionQueryError> {
self.skip_whitespace();
if !self.eat('(') {
return Err(err(format!("{TWO_SIDES}; each side opens with `(`")));
}
let mut patterns = Vec::new();
loop {
self.skip_whitespace();
match self.peek() {
Some(')') => {
self.bump();
return Ok(patterns);
}
Some('(') => patterns.push(self.parse_link_pattern()?),
Some(unexpected) => {
return Err(err(format!("unexpected `{unexpected}`; {LINK_SLOTS}")));
}
None => {
return Err(err("unbalanced parentheses: a side is missing its `)`"));
}
}
}
}
fn parse_link_pattern(&mut self) -> Result<Option<LinkPattern>, SubstitutionQueryError> {
self.bump(); self.skip_whitespace();
if self.eat(')') {
return Ok(None);
}
let first = self.parse_link_slot()?;
self.skip_whitespace();
let (index, source) = if self.eat(':') {
(Some(first), self.parse_link_slot()?)
} else {
(None, first)
};
let target = self.parse_link_slot()?;
self.skip_whitespace();
if !self.eat(')') {
return Err(err(format!(
"unbalanced parentheses, or too many slots: {LINK_SLOTS}"
)));
}
Ok(Some(LinkPattern {
index,
source,
target,
}))
}
fn parse_link_slot(&mut self) -> Result<Slot, SubstitutionQueryError> {
self.skip_whitespace();
match self.peek() {
Some('"') => Ok(Slot::Value(self.parse_string()?)),
Some('$') => {
self.bump();
let name = self.parse_token();
if name.is_empty() {
return Err(err("`$` must be followed by a variable name, as in `$i`"));
}
Ok(Slot::Variable(name))
}
_ => {
let token = self.parse_token();
if token.is_empty() {
return Err(err(LINK_SLOTS));
}
Ok(Slot::Value(token))
}
}
}
fn parse_token(&mut self) -> String {
let start = self.cursor;
while self.peek().is_some_and(is_token_char) {
self.bump();
}
self.input[start..self.cursor].to_owned()
}
}