use std::sync::Arc;
use rustc_hash::FxHashSet;
use crate::schema::types::{ElementDef, Particle, WildcardConstraint};
const EXPAND_CAP: u32 = 16;
#[derive(Debug, Clone)]
pub enum PosMatcher {
Element {
names: FxHashSet<String>,
decl_names: FxHashSet<String>,
particle_id: u32,
def: Arc<ElementDef>,
},
Wildcard(Arc<WildcardConstraint>),
}
impl PosMatcher {
fn matches(&self, name: &str, local: &str, ns: Option<&str>) -> bool {
match self {
PosMatcher::Element { names, .. } => names.contains(name) || names.contains(local),
PosMatcher::Wildcard(wc) => wc.matches(ns),
}
}
fn label(&self) -> String {
match self {
PosMatcher::Element { def, .. } => format!("'{}'", def.name),
PosMatcher::Wildcard(_) => "any element (wildcard)".to_string(),
}
}
fn plain_name(&self) -> String {
match self {
PosMatcher::Element { def, .. } => def.name.clone(),
PosMatcher::Wildcard(_) => "*".to_string(),
}
}
}
#[derive(Debug, Clone, Copy)]
struct Occur {
min: u32,
max: Option<u32>,
}
#[derive(Debug)]
pub struct ContentAutomaton {
matchers: Vec<PosMatcher>,
occurs: Vec<Occur>,
transitions: Vec<Vec<u32>>,
accepting: Vec<bool>,
pub upa_violation: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AutomatonState {
configs: smallvec::SmallVec<[(u32, u32); 2]>,
}
impl Default for AutomatonState {
fn default() -> Self {
Self {
configs: smallvec::smallvec![(0, 0)],
}
}
}
#[derive(Debug)]
pub enum FinishError {
TooFew {
name: String,
min: u32,
found: u32,
},
Missing {
expected: Vec<String>,
},
}
#[derive(Debug, PartialEq, Eq)]
pub enum StepResult {
Matched,
TooMany {
max: u32,
},
NotExpected {
expected: Vec<String>,
},
}
impl ContentAutomaton {
fn norm_run(&self, pos: usize, run: u32) -> u32 {
let occ = self.occurs[pos];
match occ.max {
None => run.min(occ.min.max(1)),
Some(_) => run,
}
}
pub fn step(
&self,
st: &mut AutomatonState,
name: &str,
local: &str,
ns: Option<&str>,
) -> StepResult {
self.step_matched(st, |pos| self.matchers[pos].matches(name, local, ns))
}
pub(crate) fn step_matched(
&self,
st: &mut AutomatonState,
matches: impl Fn(usize) -> bool,
) -> StepResult {
let mut next: smallvec::SmallVec<[(u32, u32); 2]> = smallvec::SmallVec::new();
let mut push = |state: u32, run: u32| {
if !next.contains(&(state, run)) {
next.push((state, run));
}
};
let mut blocked_max: Option<u32> = None;
for &(state, run) in &st.configs {
for &target in &self.transitions[state as usize] {
if !matches(target as usize) {
continue;
}
if target + 1 == state {
let occ = self.occurs[target as usize];
match occ.max {
Some(max) if run >= max => blocked_max = Some(max),
_ => push(state, self.norm_run(target as usize, run + 1)),
}
} else {
if state > 0 && run < self.occurs[(state - 1) as usize].min {
continue;
}
push(target + 1, 1);
}
}
}
if !next.is_empty() {
st.configs = next;
return StepResult::Matched;
}
if let Some(max) = blocked_max {
return StepResult::TooMany { max };
}
let mut expected: Vec<String> = Vec::new();
for &(state, run) in &st.configs {
for &target in &self.transitions[state as usize] {
let feasible = if target + 1 == state {
true
} else {
state == 0 || run >= self.occurs[(state - 1) as usize].min
};
if feasible {
let label = self.matchers[target as usize].label();
if !expected.contains(&label) {
expected.push(label);
}
}
}
}
StepResult::NotExpected { expected }
}
pub fn finish(&self, st: &AutomatonState) -> Result<(), FinishError> {
let mut too_few: Option<FinishError> = None;
for &(state, run) in &st.configs {
if state == 0 {
if self.accepting[0] {
return Ok(());
}
continue;
}
let pos = (state - 1) as usize;
if self.accepting[state as usize] {
if run >= self.occurs[pos].min {
return Ok(());
}
too_few.get_or_insert(FinishError::TooFew {
name: self.matchers[pos].label(),
min: self.occurs[pos].min,
found: run,
});
}
}
if let Some(err) = too_few {
return Err(err);
}
let mut expected: Vec<String> = Vec::new();
for &(state, run) in &st.configs {
for &target in &self.transitions[state as usize] {
let feasible = if target + 1 == state {
true
} else {
state == 0 || run >= self.occurs[(state - 1) as usize].min
};
if feasible {
let name = self.matchers[target as usize].plain_name();
if !expected.contains(&name) {
expected.push(name);
}
}
}
}
Err(FinishError::Missing { expected })
}
pub fn accepts_empty(&self) -> bool {
self.accepting[0]
}
pub(crate) fn matchers(&self) -> &[PosMatcher] {
&self.matchers
}
}
enum Node {
Empty,
Leaf(usize),
Seq(Vec<Node>),
Alt(Vec<Node>),
Opt(Box<Node>),
Star(Box<Node>),
}
struct Builder<'a> {
matchers: Vec<PosMatcher>,
occurs: Vec<Occur>,
subst: &'a dyn Fn(&str) -> Vec<String>,
next_particle_id: u32,
current_particle_id: Option<u32>,
}
impl Builder<'_> {
fn fresh_particle_id(&mut self) -> u32 {
match self.current_particle_id {
Some(id) => id,
None => {
let id = self.next_particle_id;
self.next_particle_id += 1;
id
}
}
}
fn leaf_element(&mut self, def: &ElementDef) -> Node {
if def.max_occurs == Some(0) {
return Node::Empty;
}
let mut names = FxHashSet::default();
let mut add = |n: &str| {
names.insert(n.to_string());
if let Some((_, local)) = n.split_once(':') {
names.insert(local.to_string());
}
};
add(&def.name);
for member in (self.subst)(&def.name) {
add(&member);
}
let mut decl_names = FxHashSet::default();
decl_names.insert(def.name.clone());
let min = def.min_occurs;
let pos = self.matchers.len();
let particle_id = self.fresh_particle_id();
self.matchers.push(PosMatcher::Element {
names,
decl_names,
particle_id,
def: Arc::new(def.clone()),
});
self.occurs.push(Occur {
min: min.max(1),
max: def.max_occurs,
});
if min == 0 {
Node::Opt(Box::new(Node::Leaf(pos)))
} else {
Node::Leaf(pos)
}
}
fn leaf_wildcard(&mut self, wc: &WildcardConstraint) -> Node {
if wc.max_occurs == Some(0) {
return Node::Empty;
}
let min = wc.min_occurs;
let pos = self.matchers.len();
self.matchers
.push(PosMatcher::Wildcard(Arc::new(wc.clone())));
self.occurs.push(Occur {
min: min.max(1),
max: wc.max_occurs,
});
if min == 0 {
Node::Opt(Box::new(Node::Leaf(pos)))
} else {
Node::Leaf(pos)
}
}
fn build_once(&mut self, particle: &Particle) -> Option<Node> {
match particle {
Particle::Element(def) => Some(self.leaf_element(def)),
Particle::Wildcard(wc) => Some(self.leaf_wildcard(wc)),
Particle::Sequence { items, .. } => {
let nodes: Vec<Node> = items.iter().filter_map(|p| self.build(p)).collect();
Some(Node::Seq(nodes))
}
Particle::Choice { items, .. } => {
let nodes: Vec<Node> = items.iter().filter_map(|p| self.build(p)).collect();
if nodes.is_empty() {
Some(Node::Empty)
} else {
Some(Node::Alt(nodes))
}
}
Particle::All { .. } => None,
}
}
fn build(&mut self, particle: &Particle) -> Option<Node> {
let (min, max) = match particle {
Particle::Element(_) | Particle::Wildcard(_) => {
return self.build_once(particle);
}
Particle::Sequence { min, max, .. } | Particle::Choice { min, max, .. } => (*min, *max),
Particle::All { .. } => return None,
};
if max == Some(0) {
return Some(Node::Empty);
}
if min == 1 && max == Some(1) {
return self.build_once(particle);
}
let id_base = self.next_particle_id;
let copy = |b: &mut Self| -> Option<Node> {
b.next_particle_id = id_base;
b.build_once(particle)
};
let req = min.min(EXPAND_CAP);
let mut parts: Vec<Node> = Vec::new();
for _ in 0..req {
parts.push(copy(self)?);
}
match max {
None => {
let star = copy(self)?;
parts.push(Node::Star(Box::new(star)));
}
Some(m) if m > min && m - min <= EXPAND_CAP && min <= EXPAND_CAP => {
let mut tail: Option<Node> = None;
for _ in 0..(m - min) {
let c = copy(self)?;
let inner = match tail.take() {
Some(t) => Node::Seq(vec![c, t]),
None => c,
};
tail = Some(Node::Opt(Box::new(inner)));
}
if let Some(t) = tail {
parts.push(t);
}
}
Some(m) if m > min => {
let star = copy(self)?;
parts.push(Node::Star(Box::new(star)));
}
_ => {} }
let node = Node::Seq(parts);
if min == 0 {
Some(Node::Opt(Box::new(node)))
} else {
Some(node)
}
}
}
struct Sets {
nullable: bool,
first: Vec<usize>,
last: Vec<usize>,
}
fn glushkov(node: &Node, follow: &mut Vec<Vec<usize>>) -> Sets {
match node {
Node::Empty => Sets {
nullable: true,
first: vec![],
last: vec![],
},
Node::Leaf(p) => Sets {
nullable: false,
first: vec![*p],
last: vec![*p],
},
Node::Opt(inner) => {
let s = glushkov(inner, follow);
Sets {
nullable: true,
..s
}
}
Node::Star(inner) => {
let s = glushkov(inner, follow);
for &l in &s.last {
for &f in &s.first {
if !follow[l].contains(&f) {
follow[l].push(f);
}
}
}
Sets {
nullable: true,
first: s.first,
last: s.last,
}
}
Node::Seq(items) => {
let mut nullable = true;
let mut first: Vec<usize> = vec![];
let mut last: Vec<usize> = vec![];
for item in items {
let s = glushkov(item, follow);
for &l in &last {
for &f in &s.first {
if !follow[l].contains(&f) {
follow[l].push(f);
}
}
}
if nullable {
first.extend(&s.first);
}
if s.nullable {
last.extend(&s.last);
} else {
last = s.last;
}
nullable &= s.nullable;
}
Sets {
nullable,
first,
last,
}
}
Node::Alt(items) => {
let mut nullable = false;
let mut first: Vec<usize> = vec![];
let mut last: Vec<usize> = vec![];
for item in items {
let s = glushkov(item, follow);
nullable |= s.nullable;
first.extend(s.first);
last.extend(s.last);
}
Sets {
nullable,
first,
last,
}
}
}
}
pub fn build_automaton(
particle: &Particle,
subst: &dyn Fn(&str) -> Vec<String>,
) -> Option<ContentAutomaton> {
let mut b = Builder {
matchers: Vec::new(),
occurs: Vec::new(),
subst,
next_particle_id: 0,
current_particle_id: None,
};
let root = b.build(particle)?;
let n = b.matchers.len();
let mut follow: Vec<Vec<usize>> = vec![Vec::new(); n];
let sets = glushkov(&root, &mut follow);
for (p, occ) in b.occurs.iter_mut().enumerate() {
if follow[p].contains(&p) {
occ.max = None;
}
}
let mut transitions: Vec<Vec<u32>> = Vec::with_capacity(n + 1);
transitions.push(sets.first.iter().map(|&p| p as u32).collect());
for f in &follow {
transitions.push(f.iter().map(|&p| p as u32).collect());
}
for (p, occ) in b.occurs.iter().enumerate() {
if occ.max != Some(1) && !transitions[p + 1].contains(&(p as u32)) {
transitions[p + 1].insert(0, p as u32);
}
}
let mut accepting = vec![false; n + 1];
accepting[0] = sets.nullable;
for &l in &sets.last {
accepting[l + 1] = true;
}
let upa_violation = check_upa(&b.matchers, &b.occurs, &transitions);
Some(ContentAutomaton {
matchers: b.matchers,
occurs: b.occurs,
transitions,
accepting,
upa_violation,
})
}
fn check_upa(
matchers: &[PosMatcher],
occurs: &[Occur],
transitions: &[Vec<u32>],
) -> Option<String> {
for (state, targets) in transitions.iter().enumerate() {
let self_pos = state.checked_sub(1).map(|p| p as u32);
let fixed_count = self_pos
.map(|p| {
let occ = occurs[p as usize];
occ.max == Some(occ.min)
})
.unwrap_or(false);
for (i, &a) in targets.iter().enumerate() {
for &bpos in &targets[i + 1..] {
if a == bpos {
continue;
}
if fixed_count && (Some(a) == self_pos || Some(bpos) == self_pos) {
continue;
}
if let (
PosMatcher::Element {
decl_names: na,
particle_id: pa,
def: da,
..
},
PosMatcher::Element {
decl_names: nb,
particle_id: pb,
..
},
) = (&matchers[a as usize], &matchers[bpos as usize])
&& pa != pb
&& na.intersection(nb).next().is_some()
{
return Some(format!(
"content model violates Unique Particle Attribution: \
element '{}' can match more than one particle",
da.name
));
}
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::types::ElementDef;
fn elem(name: &str, min: u32, max: Option<u32>) -> Particle {
let mut def = ElementDef::new(name);
def.min_occurs = min;
def.max_occurs = max;
Particle::Element(def)
}
fn no_subst(_: &str) -> Vec<String> {
Vec::new()
}
fn run(automaton: &ContentAutomaton, children: &[&str]) -> Result<(), String> {
let mut st = AutomatonState::default();
for child in children {
match automaton.step(&mut st, child, child, None) {
StepResult::Matched => {}
StepResult::TooMany { max } => {
return Err(format!("'{child}' too many (max {max})"));
}
StepResult::NotExpected { expected } => {
return Err(format!("'{child}' not expected ({expected:?})"));
}
}
}
automaton
.finish(&st)
.map_err(|e| format!("incomplete: {e:?}"))
}
#[test]
fn sequence_order_and_occurrence() {
let p = Particle::Sequence {
min: 1,
max: Some(1),
items: vec![
elem("a", 1, Some(1)),
elem("b", 0, Some(2)),
elem("c", 1, None),
],
};
let a = build_automaton(&p, &no_subst).unwrap();
assert!(a.upa_violation.is_none());
assert!(run(&a, &["a", "c"]).is_ok());
assert!(run(&a, &["a", "b", "b", "c", "c"]).is_ok());
assert!(run(&a, &["b", "c"]).is_err(), "missing required a");
assert!(run(&a, &["a", "b", "b", "b", "c"]).is_err(), "b too many");
assert!(run(&a, &["a", "c", "b"]).is_err(), "b out of order");
assert!(run(&a, &["a"]).is_err(), "missing c");
}
#[test]
fn choice_totals_across_alternatives() {
let p = Particle::Choice {
min: 2,
max: Some(2),
items: vec![elem("a", 1, Some(1)), elem("b", 1, Some(1))],
};
let a = build_automaton(&p, &no_subst).unwrap();
assert!(run(&a, &["a", "b"]).is_ok());
assert!(run(&a, &["b", "b"]).is_ok());
assert!(run(&a, &["a"]).is_err(), "only one of two");
assert!(run(&a, &["a", "b", "a"]).is_err(), "three of two");
}
#[test]
fn sequence_repeats_as_unit() {
let p = Particle::Sequence {
min: 0,
max: None,
items: vec![elem("a", 1, Some(1)), elem("b", 1, Some(1))],
};
let a = build_automaton(&p, &no_subst).unwrap();
assert!(run(&a, &[]).is_ok());
assert!(run(&a, &["a", "b"]).is_ok());
assert!(run(&a, &["a", "b", "a", "b"]).is_ok());
assert!(run(&a, &["a", "b", "a"]).is_err(), "dangling a");
assert!(run(&a, &["a", "a"]).is_err(), "a a is not a pair");
}
#[test]
fn nested_choice_in_sequence() {
let p = Particle::Sequence {
min: 1,
max: Some(1),
items: vec![
elem("a", 1, Some(1)),
Particle::Choice {
min: 1,
max: Some(1),
items: vec![elem("b", 1, Some(1)), elem("c", 1, Some(1))],
},
elem("d", 1, Some(1)),
],
};
let a = build_automaton(&p, &no_subst).unwrap();
assert!(run(&a, &["a", "b", "d"]).is_ok());
assert!(run(&a, &["a", "c", "d"]).is_ok());
assert!(run(&a, &["a", "d"]).is_err(), "choice missing");
assert!(run(&a, &["a", "b", "c", "d"]).is_err(), "both alternatives");
}
#[test]
fn upa_violation_detected() {
let p = Particle::Sequence {
min: 1,
max: Some(1),
items: vec![elem("a", 0, Some(1)), elem("a", 1, Some(1))],
};
let a = build_automaton(&p, &no_subst).unwrap();
assert!(a.upa_violation.is_some());
}
#[test]
fn all_yields_no_automaton() {
let p = Particle::All {
min: 1,
elements: vec![],
};
assert!(build_automaton(&p, &no_subst).is_none());
}
}