sup_xml_core/xsd/dfa.rs
1//! Deterministic finite automaton for content-model matching.
2//!
3//! Built once at schema-compile time from a [`ContentModel`]. At
4//! validation time the validator just walks the DFA: O(1) state lookup
5//! per child element instead of tree-walking the particle list.
6//!
7//! ## Coverage
8//!
9//! Sequence, choice, single elements, nested groups (flattened in), and
10//! wildcards all compile to DFA transitions. `xs:all` does **not** —
11//! its semantics (each particle in any order) require either subset
12//! construction (worst-case 2ⁿ states) or runtime bitset tracking. We
13//! keep the existing particle-walk matcher for all-groups; the DFA path
14//! is opt-in per ComplexType via [`ContentMatcher`].
15//!
16//! ## Determinism
17//!
18//! XSD §3.8.6 requires content models to satisfy *Unique Particle
19//! Attribution*: each child must match at most one particle without
20//! lookahead. This means the resulting state machine is naturally
21//! deterministic. The compiler enforces UPA — any duplicate
22//! transition out of the same state is a compile error.
23//!
24//! ## Substitution groups
25//!
26//! When a transition matches an element with substitutes, the
27//! substitutes are pre-expanded into separate transitions. Lookup at
28//! validation time stays a linear scan (small N typical), no
29//! per-validate substitution-group lookup needed.
30
31use std::collections::HashMap;
32use std::sync::Arc;
33
34use super::error::SchemaCompileError;
35use super::schema::{
36 ContentModel, GroupKind, MaxOccurs, NamespaceConstraint, Particle, ProcessContents,
37 QName, Term, Wildcard,
38};
39
40// ── public DFA types ─────────────────────────────────────────────────────────
41
42pub type StateId = u32;
43
44#[derive(Debug)]
45pub struct Dfa {
46 pub states: Vec<DfaState>,
47 pub initial: StateId,
48 /// Names of every element declaration that appears statically in
49 /// the source content model. Consulted by `notQName="##definedSibling"`
50 /// wildcards (XSD 1.1 §3.10.4) to exclude names declared as
51 /// siblings in the enclosing complex type. Sorted by `(namespace,
52 /// local)` so membership checks can binary-search if the list
53 /// grows; typical schemas keep this short.
54 pub defined_siblings: Arc<[QName]>,
55}
56
57#[derive(Debug, Default)]
58pub struct DfaState {
59 /// True iff the parent's content is allowed to end at this state
60 /// (all required particles satisfied so far).
61 pub accept: bool,
62 /// Element-name transitions. Each carries the [`ElementDecl`]
63 /// the matched element resolves to — stored inline so the
64 /// validator gets both the next state AND the decl in one lookup.
65 /// Linear scan — for typical N≤8 it beats a HashMap on cache use.
66 pub on_element: Vec<ElementTransition>,
67 /// Wildcard transitions — consulted *only* after element-name
68 /// transitions don't match (specific declarations take priority
69 /// over wildcards per spec).
70 pub on_wildcard: Vec<WildcardTransition>,
71}
72
73#[derive(Debug, Clone)]
74pub struct WildcardTransition {
75 pub wc: Wildcard,
76 pub next: StateId,
77 /// Synthetic transitions stand in for a `Term::GroupRef` cycle
78 /// whose referenced group can't be expanded inline. They admit
79 /// `##any` so the validator over-accepts the cycle's content;
80 /// UPA cannot meaningfully clash against them.
81 pub synthetic: bool,
82}
83
84#[derive(Debug, Clone)]
85pub struct ElementTransition {
86 pub name: QName,
87 pub next: StateId,
88 /// Resolved declaration for the matched element — its own decl
89 /// for direct matches, or the substitute's decl when the
90 /// transition was generated for a substitution-group member.
91 pub decl: Arc<super::schema::ElementDecl>,
92}
93
94impl Dfa {
95 /// Look up the next state for an incoming element with name `qn`.
96 /// Element-name transitions return both the next state and the
97 /// matched [`ElementDecl`]; wildcard transitions return the next
98 /// state and the wildcard's `processContents`.
99 ///
100 /// `wildcard_admits` decides whether a candidate wildcard accepts
101 /// `qn` — the caller supplies the wildcard-semantics policy
102 /// (positive namespace constraint plus the XSD 1.1 `notQName` /
103 /// `notNamespace` exclusions, including `##defined` and
104 /// `##definedSibling` lookups). See [`wildcard_admits`] for the
105 /// canonical implementation.
106 pub fn step(
107 &self,
108 state: StateId,
109 qn: &QName,
110 mut wildcard_admits: impl FnMut(&Wildcard, &QName) -> bool,
111 ) -> Option<DfaTransition> {
112 let s = &self.states[state as usize];
113 for t in &s.on_element {
114 if &t.name == qn {
115 return Some(DfaTransition::Element {
116 next: t.next,
117 decl: t.decl.clone(),
118 });
119 }
120 }
121 for t in &s.on_wildcard {
122 if wildcard_admits(&t.wc, qn) {
123 return Some(DfaTransition::Wildcard {
124 next: t.next,
125 process_contents: t.wc.process_contents,
126 });
127 }
128 }
129 None
130 }
131
132 pub fn is_accept(&self, state: StateId) -> bool {
133 self.states[state as usize].accept
134 }
135}
136
137#[derive(Debug)]
138pub enum DfaTransition {
139 Element { next: StateId, decl: Arc<super::schema::ElementDecl> },
140 Wildcard { next: StateId, process_contents: ProcessContents },
141}
142
143/// What a [`ComplexType`](super::types::ComplexType) uses to validate
144/// its content at runtime. The Builder picks one based on the
145/// content-model shape.
146#[derive(Debug)]
147pub enum ContentMatcher {
148 /// DFA-driven matching for sequence/choice/element/wildcard models.
149 Dfa(Arc<Dfa>),
150 /// Particle-walk matching for `xs:all` (the runtime tracks a
151 /// bitset of consumed particles).
152 All,
153 /// Empty or simple-content type — no matcher needed.
154 None,
155}
156
157/// Build a [`ContentMatcher`] for a [`ContentModel`]. Substitution
158/// groups need to be available so transitions can pre-expand to all
159/// substitutes.
160/// Set of derivation methods used to derive `child` from `ancestor`
161/// via `types`. Used at DFA-compile time to filter substitution-group
162/// members whose type derivation is blocked by their head's `block`.
163pub(super) fn derivation_methods_between(
164 child: &super::schema::TypeRef,
165 ancestor: &super::schema::TypeRef,
166 types: &HashMap<QName, super::schema::TypeRef>,
167) -> Option<super::schema::BlockSet> {
168 use super::schema::{BlockSet, TypeRef};
169 use super::types::DerivationMethod;
170 fn resolve(tr: &TypeRef, types: &HashMap<QName, TypeRef>) -> TypeRef {
171 if let TypeRef::Simple(st) = tr {
172 if let Some(name) = &st.name {
173 if let Some(rest) = name.strip_prefix("UNRESOLVED:") {
174 // Same encoding as parser::resolve_typeref_to_qname.
175 let qn = if let Some(rest) = rest.strip_prefix('{') {
176 if let Some(end) = rest.find('}') {
177 let ns = &rest[..end];
178 let local = &rest[end + 1..];
179 QName::new(if ns.is_empty() { None } else { Some(ns) }, local)
180 } else { QName::new(None, rest) }
181 } else { QName::new(None, rest) };
182 if let Some(real) = types.get(&qn) { return real.clone(); }
183 }
184 }
185 }
186 tr.clone()
187 }
188 fn eq(a: &TypeRef, b: &TypeRef) -> bool {
189 match (a, b) {
190 (TypeRef::Simple(x), TypeRef::Simple(y)) => Arc::ptr_eq(x, y) || x.name == y.name,
191 (TypeRef::Complex(x), TypeRef::Complex(y)) => Arc::ptr_eq(x, y) || x.name == y.name,
192 _ => false,
193 }
194 }
195 fn is_any_type(tr: &TypeRef) -> bool {
196 match tr {
197 TypeRef::Complex(c) => c.name.as_ref().map(|n|
198 n.namespace.as_deref() == Some(QName::XSD_NS) && &*n.local == "anyType"
199 ).unwrap_or(false),
200 _ => false,
201 }
202 }
203 let child = resolve(child, types);
204 let ancestor = resolve(ancestor, types);
205 if eq(&child, &ancestor) { return Some(BlockSet::empty()); }
206 // XSD §3.4.7 — every type, including every simple type, transitively
207 // derives from xs:anyType (the ur-type root). Substitution-group
208 // typing checks routinely use anyType as the head's type to mean
209 // "any element body is acceptable as a substitute," so this short
210 // cut keeps that working without walking a simple-type chain that
211 // doesn't carry an explicit pointer up through anySimpleType.
212 if is_any_type(&ancestor) {
213 return Some(BlockSet::RESTRICTION | BlockSet::EXTENSION);
214 }
215 if let TypeRef::Complex(ct) = &child {
216 let mut methods = BlockSet::empty();
217 let mut cur: Arc<super::types::ComplexType> = ct.clone();
218 for _ in 0..64 {
219 let d = match cur.derivation.as_ref() {
220 Some(d) => d,
221 // XSD §3.4.7 — a complex type without an explicit
222 // derivation is implicitly an extension of xs:anyType.
223 // When the ancestor we're matching is anyType itself,
224 // accumulate that final extension step and succeed.
225 None => return if is_any_type(&ancestor) {
226 Some(methods | BlockSet::EXTENSION)
227 } else { None },
228 };
229 methods |= match d.method {
230 DerivationMethod::Restriction => BlockSet::RESTRICTION,
231 DerivationMethod::Extension => BlockSet::EXTENSION,
232 };
233 let base = resolve(&d.base, types);
234 if eq(&base, &ancestor) { return Some(methods); }
235 match base {
236 TypeRef::Complex(next) => { cur = next; }
237 TypeRef::Simple(_) => return None,
238 }
239 }
240 None
241 } else if let (TypeRef::Simple(c_st), TypeRef::Simple(a_st)) = (&child, &ancestor) {
242 // XSD §3.16.6 / cos-st-derived-ok — a simple type derives from
243 // another only along a restriction chain or up to xs:anySimpleType.
244 // List and union types derive from xs:anySimpleType only; they do
245 // NOT derive from their item / member types.
246 use super::types::Variety;
247 fn is_any_simple_type(st: &super::types::SimpleType) -> bool {
248 // Two forms reach this branch: a real "xs:anySimpleType"
249 // simple-type carrier (name = "anySimpleType"), or the
250 // type_ref_for placeholder for it (the parser doesn't have
251 // a BuiltinType variant for anySimpleType so the placeholder
252 // path is taken).
253 match st.name.as_deref() {
254 Some("anySimpleType") => true,
255 Some(s) => s.starts_with("UNRESOLVED:") && s.ends_with("anySimpleType"),
256 None => false,
257 }
258 }
259 if is_any_simple_type(a_st) {
260 return Some(super::schema::BlockSet::RESTRICTION);
261 }
262 // For atomic-atomic, walk the built-in lineage. This works for
263 // the common case where the schema author derives a custom
264 // simple type with the same `builtin` field as one of its
265 // ancestors in the XSD type hierarchy.
266 match (&c_st.variety, &a_st.variety) {
267 (Variety::Atomic, Variety::Atomic) => {
268 if c_st.builtin.derives_from(a_st.builtin) {
269 Some(super::schema::BlockSet::RESTRICTION)
270 } else {
271 None
272 }
273 }
274 // List/union widen the value space relative to their items
275 // or members, so they aren't restrictions of any non-
276 // anySimpleType ancestor.
277 (Variety::List { .. }, _) | (Variety::Union { .. }, _) => None,
278 _ => None,
279 }
280 } else {
281 None
282 }
283}
284
285fn resolve_typeref_via_types(
286 tr: &super::schema::TypeRef,
287 types: &HashMap<QName, super::schema::TypeRef>,
288) -> super::schema::TypeRef {
289 use super::schema::TypeRef;
290 if let TypeRef::Simple(st) = tr {
291 if let Some(name) = &st.name {
292 if let Some(rest) = name.strip_prefix("UNRESOLVED:") {
293 let qn = if let Some(rest) = rest.strip_prefix('{') {
294 if let Some(end) = rest.find('}') {
295 let ns = &rest[..end];
296 let local = &rest[end + 1..];
297 QName::new(if ns.is_empty() { None } else { Some(ns) }, local)
298 } else { QName::new(None, rest) }
299 } else { QName::new(None, rest) };
300 if let Some(real) = types.get(&qn) { return real.clone(); }
301 }
302 }
303 }
304 tr.clone()
305}
306
307pub fn build_matcher(
308 cm: &ContentModel,
309 substitutions: &HashMap<QName, Vec<Arc<super::schema::ElementDecl>>>,
310 types: &HashMap<QName, super::schema::TypeRef>,
311) -> Result<ContentMatcher, SchemaCompileError> {
312 build_matcher_with_target_ns(cm, substitutions, types, None)
313}
314
315pub fn build_matcher_with_target_ns(
316 cm: &ContentModel,
317 substitutions: &HashMap<QName, Vec<Arc<super::schema::ElementDecl>>>,
318 types: &HashMap<QName, super::schema::TypeRef>,
319 target_ns: Option<&str>,
320) -> Result<ContentMatcher, SchemaCompileError> {
321 match cm {
322 ContentModel::Empty | ContentModel::Simple(_) => Ok(ContentMatcher::None),
323 ContentModel::Complex { root, .. } => {
324 // Top-level all-group → keep on the existing matcher.
325 if let Term::Group { kind: GroupKind::All, .. } = &root.term {
326 return Ok(ContentMatcher::All);
327 }
328 // Collect sibling-element names up front so the UPA
329 // checks against `notQName="##definedSibling"` wildcards
330 // see the full sibling set when each transition is added,
331 // not only the prefix walked so far.
332 let defined_siblings = collect_defined_siblings(root);
333 let mut b = DfaBuilder::new(defined_siblings.clone(),
334 target_ns.map(|s| Arc::<str>::from(s)));
335 let frag = b.compile_particle(root, substitutions, types)?;
336 // `frag.entries` are the start states; collapse to one
337 // explicit initial state with all entries' outgoing
338 // transitions copied in (UPA keeps this clean).
339 let initial = b.merge_into_initial(&frag)?;
340 // Every exit state is an accept state.
341 for &x in &frag.exits {
342 b.states[x as usize].accept = true;
343 }
344 // The fragment is "skippable" — accepts zero input — iff
345 // at least one entry is also one of its exits (covers
346 // choice fragments where some branch is skippable while
347 // others aren't). In that case the initial state is
348 // itself an accept state.
349 if frag.entries.iter().any(|e| frag.exits.contains(e)) {
350 b.states[initial as usize].accept = true;
351 }
352 Ok(ContentMatcher::Dfa(Arc::new(Dfa {
353 states: b.states,
354 initial,
355 defined_siblings,
356 })))
357 }
358 }
359}
360
361// ── DFA construction ─────────────────────────────────────────────────────────
362
363/// One sub-DFA fragment during compilation. Consists of the entry
364/// state(s) — where matching starts when the surrounding context hands
365/// control to this fragment — and the exit state(s) — where matching
366/// returns to the surrounding context. Entries and exits are kept as
367/// state-id sets because particles can be skipped (`minOccurs=0`),
368/// which means the entries also serve as exits for the empty path.
369struct Fragment {
370 entries: Vec<StateId>,
371 exits: Vec<StateId>,
372}
373
374struct DfaBuilder {
375 states: Vec<DfaState>,
376 /// All element names declared as siblings in the source content
377 /// model. Consulted by `wildcard_might_admit` when a wildcard
378 /// carries `notQName="##definedSibling"` (XSD 1.1 §3.10.4).
379 defined_siblings: Arc<[QName]>,
380 /// The schema's targetNamespace, used to give the `##other` /
381 /// `##targetNamespace` UPA overlap checks the precise answer
382 /// they need. `None` when the schema has no targetNamespace
383 /// (i.e., declarations live in the absent namespace).
384 target_ns: Option<Arc<str>>,
385}
386
387impl DfaBuilder {
388 fn new(defined_siblings: Arc<[QName]>, target_ns: Option<Arc<str>>) -> Self {
389 // State 0 is reserved as the global initial; we'll fill it in
390 // at the top.
391 Self { states: vec![DfaState::default()], defined_siblings, target_ns }
392 }
393
394 fn new_state(&mut self) -> StateId {
395 let id = self.states.len() as StateId;
396 self.states.push(DfaState::default());
397 id
398 }
399
400 /// Add a transition `from --(name)--> to` carrying the matched
401 /// element's declaration. Errors on UPA violation (two transitions
402 /// from the same state on the same element name).
403 fn add_element(
404 &mut self,
405 from: StateId,
406 name: QName,
407 to: StateId,
408 decl: Arc<super::schema::ElementDecl>,
409 ) -> Result<(), SchemaCompileError> {
410 let s = &mut self.states[from as usize];
411 if let Some(existing) = s.on_element.iter().find(|t| t.name == name) {
412 // Identical particle attribution is fine — same element
413 // decl seen twice from the same state happens when a
414 // bounded repeat is concatenated and the next iteration's
415 // first element overlaps with the previous iteration's
416 // last. We keep the existing transition; the resulting
417 // DFA can under-count iterations across the boundary but
418 // still attributes every accepted element to a single
419 // particle (which is what UPA actually requires).
420 if Arc::ptr_eq(&existing.decl, &decl) {
421 return Ok(());
422 }
423 return Err(SchemaCompileError::msg(format!(
424 "Unique Particle Attribution violation: element <{name}> reachable two ways from the same content-model position"
425 )));
426 }
427 // XSD §3.8.6 UPA — an incoming element-name transition can't
428 // share its source state with a wildcard that also admits the
429 // same name, unless both attribute to the same next state.
430 // Synthetic wildcards (cycle stand-ins) are excluded.
431 let target_ns = self.target_ns.as_deref();
432 for t in &s.on_wildcard {
433 if t.synthetic { continue; }
434 if t.next == to { continue; }
435 if wildcard_might_admit(&t.wc, &name, &self.defined_siblings, target_ns) {
436 return Err(SchemaCompileError::msg(format!(
437 "Unique Particle Attribution violation: element <{name}> \
438 is also admitted by a wildcard reachable from the same \
439 content-model position"
440 )));
441 }
442 }
443 s.on_element.push(ElementTransition { name, next: to, decl });
444 Ok(())
445 }
446
447 /// `synthetic` means the wildcard wasn't authored by the user —
448 /// the builder fabricates one when a `Term::GroupRef` participates
449 /// in a cycle (the inner group's shape isn't known here, so an
450 /// `##any` wildcard stands in). UPA cannot meaningfully clash
451 /// against a synthetic wildcard since the real shape lives in
452 /// the referenced group; skip UPA for those.
453 fn add_wildcard_inner(&mut self, from: StateId, wc: Wildcard, to: StateId, synthetic: bool)
454 -> Result<(), SchemaCompileError>
455 {
456 if synthetic {
457 self.states[from as usize].on_wildcard.push(WildcardTransition {
458 wc, next: to, synthetic,
459 });
460 return Ok(());
461 }
462 self.add_wildcard_checked(from, wc, to)
463 }
464
465 fn add_wildcard(&mut self, from: StateId, wc: Wildcard, to: StateId)
466 -> Result<(), SchemaCompileError>
467 {
468 self.add_wildcard_inner(from, wc, to, false)
469 }
470
471 fn add_wildcard_checked(&mut self, from: StateId, wc: Wildcard, to: StateId)
472 -> Result<(), SchemaCompileError>
473 {
474 // XSD §3.8.6 UPA — two distinct wildcards reachable from the
475 // same state with overlapping namespace sets and different
476 // next states cannot be attributed unambiguously. Wildcards
477 // sharing a `to` (which happens when concatenation glues the
478 // same particle into adjacent positions) are NOT a clash —
479 // both attributions resolve to the same particle. Synthetic
480 // cycle stand-ins are excluded.
481 let target_ns = self.target_ns.as_deref();
482 for t in &self.states[from as usize].on_wildcard {
483 if t.synthetic { continue; }
484 if t.next == to { continue; }
485 if wildcards_overlap(&t.wc, &wc, target_ns) {
486 return Err(SchemaCompileError::msg(
487 "Unique Particle Attribution violation: two wildcards reachable \
488 from the same content-model position have overlapping namespace \
489 constraints and attribute to different particles"
490 ));
491 }
492 }
493 // …and the same rule applies symmetrically against any
494 // already-recorded element-name transitions whose name the
495 // new wildcard would admit.
496 for t in &self.states[from as usize].on_element {
497 if t.next == to { continue; }
498 if wildcard_might_admit(&wc, &t.name, &self.defined_siblings, target_ns) {
499 return Err(SchemaCompileError::msg(format!(
500 "Unique Particle Attribution violation: wildcard admits \
501 element <{}> which is also reachable as a named transition \
502 from the same content-model position",
503 t.name,
504 )));
505 }
506 }
507 self.states[from as usize].on_wildcard.push(WildcardTransition {
508 wc, next: to, synthetic: false,
509 });
510 Ok(())
511 }
512
513 /// Copy every outgoing transition of `src` into `dst`. Used when
514 /// gluing sub-DFAs together — the prior fragment's exit takes over
515 /// the next fragment's entry transitions.
516 fn copy_outgoing(&mut self, src: StateId, dst: StateId)
517 -> Result<(), SchemaCompileError>
518 {
519 // Borrow checker: clone source's transition list before mutating dst.
520 let src_state = &self.states[src as usize];
521 let elems = src_state.on_element.clone();
522 let wilds = src_state.on_wildcard.clone();
523 let src_accept = src_state.accept;
524 for t in elems {
525 self.add_element(dst, t.name, t.next, t.decl)?;
526 }
527 for t in wilds {
528 self.add_wildcard_inner(dst, t.wc, t.next, t.synthetic)?;
529 }
530 if src_accept {
531 self.states[dst as usize].accept = true;
532 }
533 Ok(())
534 }
535
536 /// Build a single state collecting all entry transitions of a
537 /// fragment. Used to give the top-level Dfa one canonical
538 /// `initial`. Returns a UPA error if two entries share an
539 /// outgoing transition key.
540 fn merge_into_initial(&mut self, frag: &Fragment)
541 -> Result<StateId, SchemaCompileError>
542 {
543 let initial = 0; // reserved at construction time
544 for &e in &frag.entries {
545 self.copy_outgoing(e, initial)?;
546 }
547 Ok(initial)
548 }
549
550 // ── particle compilation ───────────────────────────────────────────
551
552 fn compile_particle(
553 &mut self,
554 p: &Particle,
555 subs: &HashMap<QName, Vec<Arc<super::schema::ElementDecl>>>,
556 types: &HashMap<QName, super::schema::TypeRef>,
557 ) -> Result<Fragment, SchemaCompileError> {
558 // XSD 1.0 §3.9.6: a particle with `maxOccurs="0"` contributes
559 // no transitions to the content model — the schema author has
560 // declared it absent. Short-circuit to an empty fragment
561 // (entry doubles as exit, no edges added) so the inner shape
562 // never reaches the state graph; otherwise the loopback paths
563 // in `repeat_fragment` keep the transitions live and we admit
564 // children the schema forbids.
565 if matches!(p.max_occurs, MaxOccurs::Bounded(0)) {
566 let s = self.new_state();
567 return Ok(Fragment { entries: vec![s], exits: vec![s] });
568 }
569 match &p.term {
570 Term::Element(decl) => {
571 // Build (name, decl) pairs for the element itself plus
572 // every member of its substitution group. Each
573 // substitute resolves to its own decl, not the head's,
574 // so the validator validates against the correct type.
575 //
576 // XSD 1.0 §3.3.4 / cvc-elt-2.2: an anchor with
577 // `block="substitution"` (or `#all`) admits no
578 // substitution-group members at all in instance
579 // documents — only the anchor's own name. Skip
580 // injecting substitutes in that case. Anchors that
581 // are themselves abstract pass through the validator's
582 // separate abstract-rejection path.
583 //
584 // §3.3.6 (cvc-elt-substitution) also says: an anchor
585 // with block="restriction" / "extension" forbids
586 // substitutes whose type derives from the anchor's
587 // type via that method. Filter those out here.
588 let mut targets: Vec<(QName, Arc<super::schema::ElementDecl>)> = Vec::new();
589 targets.push((decl.name.clone(), decl.clone()));
590 let blocks_substitution = decl.block
591 .contains(super::schema::BlockSet::SUBSTITUTION);
592 if !blocks_substitution
593 && let Some(subs_list) = subs.get(&decl.name)
594 {
595 // XSD §3.3.6 (cvc-elt-substitution): both the
596 // head element's `block` and the head element's
597 // *type*'s `block` contribute. The element's
598 // block applies methods listed on `<xs:element>`;
599 // the type's block applies methods listed on
600 // the head's `<xs:complexType>`.
601 let head_type_block: super::schema::BlockSet =
602 match resolve_typeref_via_types(&decl.type_def, types) {
603 super::schema::TypeRef::Complex(ct) => ct.block,
604 _ => super::schema::BlockSet::empty(),
605 };
606 let blocked_methods = (decl.block | head_type_block) & (
607 super::schema::BlockSet::RESTRICTION
608 | super::schema::BlockSet::EXTENSION
609 );
610 for sub in subs_list {
611 if !blocked_methods.is_empty() {
612 if let Some(used) = derivation_methods_between(
613 &sub.type_def, &decl.type_def, types,
614 ) {
615 if !(used & blocked_methods).is_empty() {
616 continue;
617 }
618 }
619 }
620 targets.push((sub.name.clone(), sub.clone()));
621 }
622 }
623 self.compile_repeated(p.min_occurs, p.max_occurs,
624 |b, from, to| {
625 for (n, d) in &targets {
626 b.add_element(from, n.clone(), to, d.clone())?;
627 }
628 Ok(())
629 })
630 }
631 Term::Wildcard(wc) => {
632 let wc = wc.clone();
633 self.compile_repeated(p.min_occurs, p.max_occurs,
634 |b, from, to| b.add_wildcard(from, wc.clone(), to))
635 }
636 Term::Group { kind, particles } => {
637 // XSD §3.8.6 — for a sequence/choice with `min > 1`,
638 // `max = unbounded` wrapping a SINGLE inner particle
639 // whose own `max = unbounded`, the effective bound is
640 // `inner_min * outer_min .. unbounded` on a chain of
641 // copies of the inner particle. The general
642 // [`repeat_group`] concat-then-loopback path drops
643 // the concat's "advance" transition when the inner
644 // already has a self-loop on the same element name
645 // (the UPA-conflict-tolerant `add_element` keeps the
646 // existing self-loop instead) — so a min=2 outer
647 // wrapping a max=unbounded inner accepts only the
648 // self-loop and never reaches the second-copy exit.
649 // Collapse the bounds and compile the inner directly
650 // when it's a single particle, sidestepping the
651 // concat boundary entirely.
652 if matches!(p.max_occurs, MaxOccurs::Unbounded)
653 && p.min_occurs > 1
654 && particles.len() == 1
655 && matches!(kind, GroupKind::Sequence | GroupKind::Choice)
656 {
657 let inner = &particles[0];
658 let inner_min = inner.min_occurs.saturating_mul(p.min_occurs);
659 let collapsed = Particle {
660 term: inner.term.clone(),
661 min_occurs: inner_min,
662 max_occurs: MaxOccurs::Unbounded,
663 };
664 return self.compile_particle(&collapsed, subs, types);
665 }
666 let build_inner = |this: &mut Self| -> Result<Fragment, SchemaCompileError> {
667 match kind {
668 GroupKind::Sequence => this.compile_sequence(particles, subs, types),
669 GroupKind::Choice => this.compile_choice(particles, subs, types),
670 GroupKind::All => Err(SchemaCompileError::msg(
671 "nested xs:all is not supported in v1 (use sequence/choice)"
672 )),
673 }
674 };
675 self.repeat_group(p.min_occurs, p.max_occurs, build_inner)
676 }
677 Term::GroupRef(_name) => {
678 // An unresolved GroupRef at DFA build time means the
679 // ref participates in a cycle that crosses an
680 // element boundary (the `resolve_group_refs` pass
681 // leaves these intact rather than recursing forever).
682 //
683 // Model the cycle position as a wildcard. The
684 // particle's own `min_occurs` / `max_occurs` apply
685 // to the WHOLE referenced group; the effective range
686 // contributed by this position is
687 // `(p.min * inner_min, p.max * inner_max)`. Since
688 // the inner is unknown — it could collapse to zero
689 // elements at runtime or expand arbitrarily — we
690 // use `(0, ∞)` regardless of `p`'s outer bounds.
691 // Over-accept rather than mis-reject.
692 let _ = p.min_occurs;
693 let _ = p.max_occurs;
694 let wc = Wildcard {
695 namespaces: NamespaceConstraint::Any,
696 process_contents: ProcessContents::Lax,
697 not_qnames: Vec::new(),
698 not_namespaces: Vec::new(),
699 not_qname_defined: false,
700 not_qname_defined_sibling: false,
701 };
702 self.compile_repeated(0, MaxOccurs::Unbounded,
703 |b, from, to| b.add_wildcard_inner(from, wc.clone(), to, true))
704 }
705 }
706 }
707
708 /// Build a fragment for a single transition repeated `min..max`
709 /// times. Used by both element and wildcard particles. The
710 /// `add` closure plugs the transition definition into a state pair.
711 fn compile_repeated<F>(
712 &mut self,
713 min: u32,
714 max: MaxOccurs,
715 mut add: F,
716 ) -> Result<Fragment, SchemaCompileError>
717 where
718 F: FnMut(&mut Self, StateId, StateId) -> Result<(), SchemaCompileError>,
719 {
720 // Build a chain of states: s0 -> s1 -> ... -> s_n on the
721 // element name. States from index `min` onward are exits.
722 // For unbounded: s0 -> s1 -> s1 (self-loop after min).
723 //
724 // Bounded maxOccurs larger than `LARGE_BOUND` is treated as
725 // unbounded: a 9_999_999-state chain blows up DFA size and
726 // memory for negligible practical benefit (the loopback
727 // accepts everything the chain would up to that point and
728 // more — over-accepting beyond max instead of mis-rejecting
729 // before max). Real schemas almost never use precise upper
730 // bounds higher than a few dozen.
731 const LARGE_BOUND: u32 = 128;
732 let treat_as_unbounded = matches!(max, MaxOccurs::Unbounded)
733 || matches!(max, MaxOccurs::Bounded(n) if n > LARGE_BOUND);
734 let max_n: u32 = match max {
735 MaxOccurs::Unbounded => u32::MAX, // sentinel
736 MaxOccurs::Bounded(n) => n,
737 };
738 let chain_len = if treat_as_unbounded {
739 min.max(1)
740 } else {
741 max_n
742 };
743 let chain_len = chain_len.min(LARGE_BOUND);
744
745 let mut state_ids = Vec::with_capacity(chain_len as usize + 1);
746 for _ in 0..=chain_len {
747 state_ids.push(self.new_state());
748 }
749 for i in 0..chain_len as usize {
750 add(self, state_ids[i], state_ids[i + 1])?;
751 }
752 if treat_as_unbounded {
753 // Self-loop on the last state.
754 let last = *state_ids.last().unwrap();
755 add(self, last, last)?;
756 }
757
758 let entries = vec![state_ids[0]];
759 let exits = if min == 0 {
760 // Skippable: entry itself is also an exit.
761 let mut e = vec![state_ids[0]];
762 e.extend_from_slice(&state_ids[1..]);
763 e
764 } else {
765 state_ids[(min as usize).min(state_ids.len() - 1)..].to_vec()
766 };
767 Ok(Fragment { entries, exits })
768 }
769
770 fn compile_sequence(
771 &mut self,
772 particles: &[Particle],
773 subs: &HashMap<QName, Vec<Arc<super::schema::ElementDecl>>>,
774 types: &HashMap<QName, super::schema::TypeRef>,
775 ) -> Result<Fragment, SchemaCompileError> {
776 if particles.is_empty() {
777 // Empty sequence → fragment that accepts immediately.
778 let s = self.new_state();
779 return Ok(Fragment { entries: vec![s], exits: vec![s] });
780 }
781
782 let first = self.compile_particle(&particles[0], subs, types)?;
783 if particles.len() == 1 {
784 return Ok(first);
785 }
786
787 let mut current = first;
788 for p in &particles[1..] {
789 let next = self.compile_particle(p, subs, types)?;
790 current = self.concat(current, next)?;
791 }
792 Ok(current)
793 }
794
795 /// Concatenate two fragments — every exit of the first picks up
796 /// the entries' transitions of the second.
797 fn concat(&mut self, a: Fragment, b: Fragment)
798 -> Result<Fragment, SchemaCompileError>
799 {
800 // For each exit of a, copy each entry of b's outgoing transitions in.
801 for &exit in &a.exits {
802 for &entry in &b.entries {
803 self.copy_outgoing(entry, exit)?;
804 }
805 }
806 // The combined fragment's entries are a's entries. If a was
807 // skippable (entry ∈ exits), the combined fragment can start
808 // by using b's entries — handled implicitly by the
809 // `merge_into_initial` step.
810 let entries = a.entries.clone();
811 // Combined exits: b's exits, plus a's exits if b is skippable.
812 let mut exits = b.exits.clone();
813 if b.entries.iter().any(|e| b.exits.contains(e)) {
814 for e in &a.exits {
815 if !exits.contains(e) { exits.push(*e); }
816 }
817 }
818 Ok(Fragment { entries, exits })
819 }
820
821 fn compile_choice(
822 &mut self,
823 particles: &[Particle],
824 subs: &HashMap<QName, Vec<Arc<super::schema::ElementDecl>>>,
825 types: &HashMap<QName, super::schema::TypeRef>,
826 ) -> Result<Fragment, SchemaCompileError> {
827 if particles.is_empty() {
828 let s = self.new_state();
829 return Ok(Fragment { entries: vec![s], exits: vec![s] });
830 }
831 let mut entries = Vec::new();
832 let mut exits = Vec::new();
833 for p in particles {
834 let frag = self.compile_particle(p, subs, types)?;
835 entries.extend(frag.entries);
836 exits.extend(frag.exits);
837 }
838 Ok(Fragment { entries, exits })
839 }
840
841 /// Wrap a fragment in min/max occurrence handling — used when an
842 /// `xs:sequence` or `xs:choice` itself has `minOccurs`/`maxOccurs`
843 /// other than 1. For v1 we support the common cases (max=1 + min
844 /// ≥ 0; arbitrary min with unbounded max via a self-concat). Other
845 /// shapes return the unwrapped fragment with a documented note.
846 /// Apply `minOccurs` / `maxOccurs` to a model-group particle.
847 ///
848 /// Strategy: when `minOccurs > 1`, we *concatenate* `min` fresh
849 /// copies of the inner fragment so the validator can't shortcut
850 /// through fewer iterations. For everything after the minimum we
851 /// fall back to the cheap loopback (treating the tail as
852 /// unbounded) — this can over-accept when `maxOccurs` is bounded,
853 /// but never crashes with state-explosion and avoids the spurious
854 /// UPA conflicts that a full unroll triggers when an inner
855 /// element name overlaps between consecutive iterations.
856 fn repeat_group<F>(
857 &mut self,
858 min: u32,
859 max: MaxOccurs,
860 mut build_inner: F,
861 ) -> Result<Fragment, SchemaCompileError>
862 where
863 F: FnMut(&mut Self) -> Result<Fragment, SchemaCompileError>,
864 {
865 if min <= 1 {
866 let inner = build_inner(self)?;
867 return self.repeat_fragment(inner, min, max);
868 }
869 // Cap the mandatory copy count. XSD permits arbitrary
870 // values, but anything beyond a handful is either pathological
871 // or wraps a large content model — in both cases we'd rather
872 // accept a few extra (over-permissive) instances than blow up
873 // DFA state count.
874 let required = (min as usize).min(8);
875
876 let mut current = build_inner(self)?;
877 let mut last_entries = current.entries.clone();
878 for _ in 1..required {
879 let next = build_inner(self)?;
880 last_entries = next.entries.clone();
881 current = self.concat(current, next)?;
882 }
883 // After the mandatory copies, loop the final iteration back
884 // to itself so the validator can take any number of
885 // additional rounds — bounded `max` becomes over-permissive
886 // (we accept a few extra iterations the spec forbids) but
887 // we never under-accept. The loopback uses the *last*
888 // copy's entries (which match the current exits' position
889 // in iteration space) so the targets remain accepting.
890 let _ = max;
891 let pseudo = Fragment { entries: last_entries, exits: current.exits.clone() };
892 self.add_loopback_transitions(&pseudo)?;
893 Ok(current)
894 }
895
896 fn repeat_fragment(
897 &mut self,
898 frag: Fragment,
899 min: u32,
900 max: MaxOccurs,
901 ) -> Result<Fragment, SchemaCompileError> {
902 // Single occurrence — most common case.
903 if min == 1 && matches!(max, MaxOccurs::Bounded(1)) {
904 return Ok(frag);
905 }
906 // Optional (0..=1) — entry doubles as exit.
907 if min == 0 && matches!(max, MaxOccurs::Bounded(1)) {
908 let mut frag = frag;
909 for &e in &frag.entries.clone() {
910 if !frag.exits.contains(&e) {
911 frag.exits.push(e);
912 }
913 }
914 return Ok(frag);
915 }
916 // Unbounded — splice each entry's outgoing transitions onto
917 // every exit so the fragment can be re-entered. For min=0
918 // entries are also exits (zero iterations allowed).
919 if matches!(max, MaxOccurs::Unbounded) {
920 let mut frag = frag;
921 self.add_loopback_transitions(&frag)?;
922 if min == 0 {
923 for &e in &frag.entries.clone() {
924 if !frag.exits.contains(&e) {
925 frag.exits.push(e);
926 }
927 }
928 }
929 return Ok(frag);
930 }
931 // Bounded re-iteration (max ≥ 2): exact bound enforcement
932 // would require either an NFA → DFA pass or a state-per-
933 // iteration unroll, both of which interact badly with our
934 // single-transition-per-(state, name) UPA check. The
935 // pragmatic tradeoff: treat the upper bound as unbounded
936 // (a few extra iterations may slip through) but enforce
937 // the lower bound exactly via the caller's mandatory-copy
938 // concat. For `min == 0 || min == 1` reps the optional
939 // first iteration also needs entries to be exits.
940 let mut frag = frag;
941 self.add_loopback_transitions(&frag)?;
942 if min == 0 {
943 for &e in &frag.entries.clone() {
944 if !frag.exits.contains(&e) {
945 frag.exits.push(e);
946 }
947 }
948 }
949 Ok(frag)
950 }
951
952 /// For every (exit, entry) pair in the fragment, copy the entry's
953 /// outgoing element + wildcard transitions onto the exit. This
954 /// turns the fragment into a Kleene-plus loop. Skipped when
955 /// entry == exit (the loop is already implicit on that state).
956 fn add_loopback_transitions(&mut self, frag: &Fragment) -> Result<(), SchemaCompileError> {
957 for &exit in &frag.exits {
958 for &entry in &frag.entries {
959 if exit == entry { continue; }
960 let entry_elems = self.states[entry as usize].on_element.clone();
961 let entry_wilds = self.states[entry as usize].on_wildcard.clone();
962 let exit_state = &mut self.states[exit as usize];
963 for t in entry_elems {
964 if !exit_state.on_element.iter().any(|e| e.name == t.name) {
965 exit_state.on_element.push(t);
966 }
967 }
968 for t in entry_wilds {
969 let already_present = exit_state.on_wildcard.iter()
970 .any(|other| std::ptr::eq(&other.wc as *const _, &t.wc as *const _));
971 if !already_present {
972 exit_state.on_wildcard.push(t);
973 }
974 }
975 }
976 }
977 Ok(())
978 }
979}
980
981/// Conservative test: does the wildcard's positive namespace
982/// constraint admit `qn`? An exact `notQName="…"` exclusion is
983/// honoured; `notNamespace` and the `##defined` / `##definedSibling`
984/// tokens require schema context the DFA builder doesn't have, so
985/// we ignore them here — the conservative answer (treat as admitted)
986/// is what makes the UPA check report the clash rather than miss it.
987fn wildcard_might_admit(
988 wc: &Wildcard,
989 qn: &QName,
990 defined_siblings: &[QName],
991 target_ns: Option<&str>,
992) -> bool {
993 use NamespaceConstraint::*;
994 // notQName= literal QNames is the one exclusion we can apply
995 // unambiguously at compile time.
996 if !qn.local.is_empty() {
997 for forbidden in &wc.not_qnames {
998 if forbidden.namespace == qn.namespace && forbidden.local == qn.local {
999 return false;
1000 }
1001 }
1002 }
1003 // `##definedSibling` excludes the names that the source content
1004 // model declares as siblings of this wildcard — the same set
1005 // the validator consults at instance time.
1006 if wc.not_qname_defined_sibling && defined_siblings.iter().any(|s| s == qn) {
1007 return false;
1008 }
1009 let ns = qn.namespace.as_deref();
1010 match &wc.namespaces {
1011 Any => true,
1012 Other => ns.is_some() && ns != target_ns,
1013 List(allowed) => allowed.iter().any(|item| match (item.as_deref(), ns) {
1014 (None, None) => true,
1015 (Some(a), Some(b)) => a == b,
1016 _ => false,
1017 }),
1018 }
1019}
1020
1021/// Conservative overlap predicate between two wildcards' namespace
1022/// constraints: returns true when there exists some namespace that
1023/// both constraints admit. Used to detect UPA violations between
1024/// distinct wildcards at the same DFA state. Notes:
1025///
1026/// * `notNamespace` / `notQName` exclusions are not subtracted here —
1027/// excluding from one side without excluding from the other can
1028/// still leave overlap, so the conservative answer is "overlap".
1029fn wildcards_overlap(a: &Wildcard, b: &Wildcard, target_ns: Option<&str>) -> bool {
1030 use NamespaceConstraint::*;
1031 // `##other` admits a list entry iff that entry is some non-target
1032 // URI. An entry equal to the targetNamespace or `None`
1033 // (no-namespace) is excluded from `##other`.
1034 fn list_admits_other(list: &[Option<Arc<str>>], target_ns: Option<&str>) -> bool {
1035 list.iter().any(|entry| match entry.as_deref() {
1036 None => false,
1037 Some(ns) => target_ns != Some(ns),
1038 })
1039 }
1040 match (&a.namespaces, &b.namespaces) {
1041 (Any, _) | (_, Any) => true,
1042 (Other, Other) => true,
1043 (Other, List(l)) | (List(l), Other) => list_admits_other(l, target_ns),
1044 (List(la), List(lb)) => la.iter().any(|x| lb.iter().any(|y| x == y)),
1045 }
1046}
1047
1048// ── helpers shared with the validator ────────────────────────────────────────
1049
1050/// Full XSD 1.1 §3.10.4 wildcard-match decision: does the wildcard
1051/// admit `qn` once positive namespace and all `notNamespace`,
1052/// `notQName`, `##defined`, and `##definedSibling` exclusions are
1053/// applied?
1054///
1055/// `is_defined` is consulted only when the wildcard's `notQName`
1056/// carries `##defined` (XSD 1.1: "any element/attribute with a
1057/// top-level declaration of this kind in the schema"). Element
1058/// wildcards pass `|q| schema.element(q).is_some()`; attribute
1059/// wildcards pass `|q| schema.attribute(q).is_some()`.
1060///
1061/// `is_sibling` is consulted only when `notQName` carries
1062/// `##definedSibling` — the set of element/attribute names declared
1063/// as siblings in the enclosing complex type. For element wildcards
1064/// the DFA caches this on [`Dfa::defined_siblings`]; for attribute
1065/// wildcards the caller walks `ComplexType::attributes`.
1066pub(super) fn wildcard_admits(
1067 wc: &Wildcard,
1068 qn: &QName,
1069 target_ns: Option<&str>,
1070 is_defined: impl FnOnce(&QName) -> bool,
1071 is_sibling: impl FnOnce(&QName) -> bool,
1072) -> bool {
1073 let ns = qn.namespace.as_deref();
1074 let ns_ok = match &wc.namespaces {
1075 NamespaceConstraint::Any => true,
1076 NamespaceConstraint::Other => ns != target_ns && ns.is_some(),
1077 NamespaceConstraint::List(allowed) => allowed.iter().any(|item| {
1078 match (item.as_deref(), ns) {
1079 (None, None) => true,
1080 (Some(a), Some(b)) => a == b,
1081 _ => false,
1082 }
1083 }),
1084 };
1085 if !ns_ok { return false; }
1086 for item in &wc.not_namespaces {
1087 match (item.as_deref(), ns) {
1088 (None, None) => return false,
1089 (Some(a), Some(b)) if a == b => return false,
1090 _ => {}
1091 }
1092 }
1093 if !qn.local.is_empty() {
1094 for forbidden in &wc.not_qnames {
1095 if forbidden.namespace == qn.namespace && forbidden.local == qn.local {
1096 return false;
1097 }
1098 }
1099 }
1100 if wc.not_qname_defined && is_defined(qn) {
1101 return false;
1102 }
1103 if wc.not_qname_defined_sibling && is_sibling(qn) {
1104 return false;
1105 }
1106 true
1107}
1108
1109/// Collect the names of every `Term::Element` particle reachable from
1110/// `root` — the static `##definedSibling` set for the enclosing
1111/// complex type's content model. Walks through nested groups; does
1112/// not expand substitution-group members (those are independent
1113/// top-level declarations, not sibling declarations of this type).
1114fn collect_defined_siblings(root: &Particle) -> Arc<[QName]> {
1115 let mut out: Vec<QName> = Vec::new();
1116 fn walk(p: &Particle, out: &mut Vec<QName>) {
1117 match &p.term {
1118 Term::Element(decl) => {
1119 if !out.iter().any(|n| n == &decl.name) {
1120 out.push(decl.name.clone());
1121 }
1122 }
1123 Term::Group { particles, .. } => {
1124 for c in particles.iter() { walk(c, out); }
1125 }
1126 Term::Wildcard(_) | Term::GroupRef(_) => {}
1127 }
1128 }
1129 walk(root, &mut out);
1130 out.sort_by(|a, b| (a.namespace.as_deref(), a.local.as_ref())
1131 .cmp(&(b.namespace.as_deref(), b.local.as_ref())));
1132 out.into()
1133}
1134
1135#[cfg(test)]
1136mod tests {
1137 use super::*;
1138 use crate::xsd::schema::{ElementDecl, BlockSet};
1139 use crate::xsd::types::SimpleType;
1140 use crate::xsd::BuiltinType;
1141
1142 fn elem_decl(name: &str) -> Arc<ElementDecl> {
1143 Arc::new(ElementDecl {
1144 name: QName::new(None, name),
1145 type_def: super::super::TypeRef::Simple(Arc::new(SimpleType::of_builtin(BuiltinType::String))),
1146 nillable: false,
1147 default: None, fixed: None,
1148 abstract_: false, substitution_group: None,
1149 block: BlockSet::default(), final_: BlockSet::default(),
1150 identity: Vec::new(),
1151 })
1152 }
1153
1154 fn one_of(name: &str) -> Particle {
1155 Particle {
1156 min_occurs: 1, max_occurs: MaxOccurs::Bounded(1),
1157 term: Term::Element(elem_decl(name)),
1158 }
1159 }
1160
1161 fn unbounded(name: &str) -> Particle {
1162 Particle {
1163 min_occurs: 1, max_occurs: MaxOccurs::Unbounded,
1164 term: Term::Element(elem_decl(name)),
1165 }
1166 }
1167
1168 fn optional(name: &str) -> Particle {
1169 Particle {
1170 min_occurs: 0, max_occurs: MaxOccurs::Bounded(1),
1171 term: Term::Element(elem_decl(name)),
1172 }
1173 }
1174
1175 fn sequence(particles: Vec<Particle>) -> ContentModel {
1176 ContentModel::Complex {
1177 root: Particle {
1178 min_occurs: 1, max_occurs: MaxOccurs::Bounded(1),
1179 term: Term::Group { kind: GroupKind::Sequence, particles: particles.into() },
1180 },
1181 mixed: false,
1182 }
1183 }
1184
1185 fn choice(particles: Vec<Particle>) -> ContentModel {
1186 ContentModel::Complex {
1187 root: Particle {
1188 min_occurs: 1, max_occurs: MaxOccurs::Bounded(1),
1189 term: Term::Group { kind: GroupKind::Choice, particles: particles.into() },
1190 },
1191 mixed: false,
1192 }
1193 }
1194
1195 fn matcher(cm: &ContentModel) -> Arc<Dfa> {
1196 match build_matcher(cm, &HashMap::new(), &HashMap::new()).unwrap() {
1197 ContentMatcher::Dfa(d) => d,
1198 _ => panic!("expected DFA"),
1199 }
1200 }
1201
1202 fn run<'a>(dfa: &Dfa, names: impl IntoIterator<Item = &'a str>) -> Option<bool> {
1203 let mut s = dfa.initial;
1204 for n in names {
1205 let qn = QName::new(None, n);
1206 let step = dfa.step(s, &qn, |wc, qn| {
1207 wildcard_admits(wc, qn, None, |_| false, |q| {
1208 dfa.defined_siblings.iter().any(|n| n == q)
1209 })
1210 });
1211 match step {
1212 Some(DfaTransition::Element { next, .. }) => s = next,
1213 Some(DfaTransition::Wildcard { next, .. }) => s = next,
1214 None => return None,
1215 }
1216 }
1217 Some(dfa.is_accept(s))
1218 }
1219
1220 #[test]
1221 fn sequence_single_element() {
1222 let cm = sequence(vec![one_of("a")]);
1223 let dfa = matcher(&cm);
1224 assert_eq!(run(&dfa, ["a"]), Some(true));
1225 assert_eq!(run(&dfa, []), Some(false)); // missing
1226 assert_eq!(run(&dfa, ["a", "a"]), None); // too many
1227 assert_eq!(run(&dfa, ["b"]), None); // wrong name
1228 }
1229
1230 #[test]
1231 fn sequence_multiple_elements() {
1232 let cm = sequence(vec![one_of("a"), one_of("b"), one_of("c")]);
1233 let dfa = matcher(&cm);
1234 assert_eq!(run(&dfa, ["a", "b", "c"]), Some(true));
1235 assert_eq!(run(&dfa, ["a", "b"]), Some(false)); // c missing
1236 assert_eq!(run(&dfa, ["a", "c"]), None); // wrong order
1237 assert_eq!(run(&dfa, ["b", "a", "c"]), None); // wrong order
1238 }
1239
1240 #[test]
1241 fn sequence_with_unbounded() {
1242 let cm = sequence(vec![unbounded("item")]);
1243 let dfa = matcher(&cm);
1244 assert_eq!(run(&dfa, ["item"]), Some(true));
1245 assert_eq!(run(&dfa, ["item", "item", "item"]), Some(true));
1246 assert_eq!(run(&dfa, []), Some(false)); // min 1 unmet
1247 }
1248
1249 #[test]
1250 fn sequence_with_optional_tail() {
1251 let cm = sequence(vec![one_of("a"), optional("b")]);
1252 let dfa = matcher(&cm);
1253 assert_eq!(run(&dfa, ["a"]), Some(true));
1254 assert_eq!(run(&dfa, ["a", "b"]), Some(true));
1255 assert_eq!(run(&dfa, []), Some(false));
1256 }
1257
1258 #[test]
1259 fn choice_first_branch() {
1260 let cm = choice(vec![one_of("a"), one_of("b")]);
1261 let dfa = matcher(&cm);
1262 assert_eq!(run(&dfa, ["a"]), Some(true));
1263 assert_eq!(run(&dfa, ["b"]), Some(true));
1264 assert_eq!(run(&dfa, ["c"]), None);
1265 assert_eq!(run(&dfa, ["a", "b"]), None); // can't take both branches
1266 }
1267
1268 #[test]
1269 fn upa_violation_is_compile_error() {
1270 // Two particles in a choice with the same name → ambiguous.
1271 let cm = choice(vec![one_of("a"), one_of("a")]);
1272 let r = build_matcher(&cm, &HashMap::new(), &HashMap::new());
1273 assert!(r.is_err(), "expected UPA error");
1274 }
1275}