Skip to main content

relax_ng/
simplify.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3
4use crate::SchemaResolver;
5use crate::grammar::{Combine, Grammar, GrammarItem, NameClass, Pattern, Root, SchemaDocument};
6use crate::syntax::parse_with_inherited_ns;
7
8/// A parsed schema after composition/simplification (RELAX NG spec §4):
9/// `include`/`div`/`combine` resolved, nested `grammar`s flattened, every
10/// pattern reduced to the handful of primitive forms the validator
11/// understands. Produced by [`simplify`] (or [`crate::Schema::compile`],
12/// which also parses); pass it to [`crate::validate`] (or
13/// [`crate::Schema::validate`]) to check documents against it.
14#[derive(Clone, Debug)]
15pub struct CompiledSchema {
16    pub(crate) start: Pattern,
17    pub(crate) definitions: BTreeMap<String, Pattern>,
18}
19
20impl CompiledSchema {
21    /// How many named `define`s survived simplification — mainly useful
22    /// for diagnostics/tests, not something normal validation needs.
23    pub fn definition_count(&self) -> usize {
24        self.definitions.len()
25    }
26    /// Whether this schema's `start` pattern can match anything at all
27    /// (`false` only for a schema that's valid RELAX NG but pointless —
28    /// its `start` simplified all the way down to `notAllowed`).
29    pub fn has_start(&self) -> bool {
30        !matches!(self.start, Pattern::NotAllowed)
31    }
32}
33
34/// A schema parsed without error but invalid once composed/simplified —
35/// an undefined `ref`, illegal recursion, a duplicate `define` with
36/// inconsistent `combine`, a §7 restriction violation, and so on.
37#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct SchemaError {
39    message: String,
40}
41
42impl SchemaError {
43    fn new(message: impl Into<String>) -> Self {
44        Self {
45            message: message.into(),
46        }
47    }
48}
49
50impl fmt::Display for SchemaError {
51    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
52        formatter.write_str(&self.message)
53    }
54}
55
56impl std::error::Error for SchemaError {}
57
58/// Composes and simplifies `document` (RELAX NG spec §4) into a
59/// [`CompiledSchema`], resolving any `include`/`externalRef` it uses via
60/// `resolver`. See [`crate::Schema::compile`] for a convenience wrapper
61/// that also parses.
62pub fn simplify(
63    document: &SchemaDocument,
64    resolver: &impl SchemaResolver,
65) -> Result<CompiledSchema, SchemaError> {
66    let mut loading = BTreeSet::new();
67    simplify_root(document, resolver, &mut loading)
68}
69
70fn simplify_root(
71    document: &SchemaDocument,
72    resolver: &impl SchemaResolver,
73    loading: &mut BTreeSet<String>,
74) -> Result<CompiledSchema, SchemaError> {
75    let compiled = match &document.root {
76        Root::Pattern(pattern) => {
77            let mut extra = BTreeMap::new();
78            let mut next_id = 0usize;
79            let start = simplify_pattern(
80                pattern,
81                resolver,
82                loading,
83                &mut extra,
84                &mut next_id,
85                &BTreeSet::new(),
86                None,
87            )?;
88            Ok(CompiledSchema {
89                start,
90                definitions: extra,
91            })
92        }
93        Root::Grammar(grammar) => simplify_grammar(grammar, resolver, loading),
94    }?;
95    check_illegal_recursion(&compiled.start, &compiled.definitions)?;
96    let mut scope = RestrictionScope {
97        definitions: &compiled.definitions,
98        visiting: BTreeSet::new(),
99        verified: BTreeSet::new(),
100    };
101    check_restrictions(&compiled.start, false, false, false, false, &mut scope)?;
102    // `ref` is transparent above, so every definition reachable from
103    // `start` was already checked under its *actual* ancestor context
104    // (which is what §7.1-§7.3 restrict) — a `define` is free to rely on
105    // every one of its `ref` sites providing e.g. a `oneOrMore` ancestor,
106    // as real-world schemas do. A definition unreachable from `start` has
107    // no such context to inherit, so it's checked standalone instead.
108    let mut reachable = BTreeSet::new();
109    collect_reachable_defines(&compiled.start, &compiled.definitions, &mut reachable);
110    for (def_name, pattern) in compiled.definitions.iter() {
111        if reachable.contains(def_name.as_str()) {
112            continue;
113        }
114        check_restrictions(pattern, false, false, false, false, &mut scope)?;
115    }
116    check_group_restrictions(&compiled.start, &compiled.definitions)?;
117    for pattern in compiled.definitions.values() {
118        check_group_restrictions(pattern, &compiled.definitions)?;
119    }
120    check_start_restrictions(&compiled.start, &compiled.definitions, &mut BTreeSet::new())?;
121    Ok(compiled)
122}
123
124/// RELAX NG §4.19: a `define` must not be able to reach itself again
125/// through a chain of `ref`s without an intervening `element` — that
126/// recursion would never ground out. Legal recursion (through an
127/// `element`) is unaffected, since we don't follow `ref`s found inside an
128/// `element`'s own body when building the dependency graph below.
129///
130/// Unlike §4.16, this is only checked for `define`s actually reachable
131/// from `start` — a self-referencing `define` that's never used is dead
132/// code, not an infinite loop, and the spec test suite confirms it's
133/// still a *correct* schema.
134fn check_illegal_recursion(
135    start: &Pattern,
136    definitions: &BTreeMap<String, Pattern>,
137) -> Result<(), SchemaError> {
138    let mut reachable = BTreeSet::new();
139    collect_reachable_defines(start, definitions, &mut reachable);
140    let mut graph: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
141    for name in &reachable {
142        let pattern = &definitions[*name];
143        let mut edges = Vec::new();
144        collect_ref_edges(pattern, &mut edges);
145        graph.insert(*name, edges);
146    }
147    let mut state: BTreeMap<&str, RefVisit> = BTreeMap::new();
148    for name in graph.keys().copied().collect::<Vec<_>>() {
149        if !state.contains_key(name) {
150            visit_ref_graph(name, &graph, &mut state)?;
151        }
152    }
153    Ok(())
154}
155
156/// Collects the names of every `define` reachable (transitively, through
157/// `ref`, crossing `element` freely) from `pattern`.
158fn collect_reachable_defines<'a>(
159    pattern: &'a Pattern,
160    definitions: &'a BTreeMap<String, Pattern>,
161    reachable: &mut BTreeSet<&'a str>,
162) {
163    match pattern {
164        Pattern::Ref(name) => {
165            if let Some(definition) = definitions.get(name)
166                && reachable.insert(name.as_str())
167            {
168                collect_reachable_defines(definition, definitions, reachable);
169            }
170        }
171        Pattern::Element { body, .. } | Pattern::Attribute { body, .. } => {
172            for child in body {
173                collect_reachable_defines(child, definitions, reachable);
174            }
175        }
176        Pattern::Group(values)
177        | Pattern::Interleave(values)
178        | Pattern::Choice(values)
179        | Pattern::OneOrMore(values)
180        | Pattern::List(values)
181        // Present only pre-simplification (Optional/ZeroOrMore/Mixed are
182        // rewritten away by then) — harmless to include unconditionally.
183        | Pattern::Optional(values)
184        | Pattern::ZeroOrMore(values)
185        | Pattern::Mixed(values) => {
186            for child in values {
187                collect_reachable_defines(child, definitions, reachable);
188            }
189        }
190        Pattern::Data { except, .. } => {
191            for child in except {
192                collect_reachable_defines(child, definitions, reachable);
193            }
194        }
195        _ => {}
196    }
197}
198
199#[derive(Clone, Copy, PartialEq, Eq)]
200enum RefVisit {
201    Visiting,
202    Done,
203}
204
205fn visit_ref_graph<'a>(
206    name: &'a str,
207    graph: &BTreeMap<&'a str, Vec<&'a str>>,
208    state: &mut BTreeMap<&'a str, RefVisit>,
209) -> Result<(), SchemaError> {
210    state.insert(name, RefVisit::Visiting);
211    if let Some(edges) = graph.get(name) {
212        for &target in edges {
213            match state.get(target) {
214                Some(RefVisit::Visiting) => {
215                    return Err(SchemaError::new(format!(
216                        "RELAX NG §4.19: `{target}` illegally refers to itself without an intervening element"
217                    )));
218                }
219                Some(RefVisit::Done) => {}
220                None if graph.contains_key(target) => {
221                    visit_ref_graph(target, graph, state)?;
222                }
223                None => {}
224            }
225        }
226    }
227    state.insert(name, RefVisit::Done);
228    Ok(())
229}
230
231/// Collects `ref` targets reachable from `pattern` without crossing an
232/// `element` boundary (transparent through `group`/`interleave`/`choice`/
233/// `oneOrMore`; `attribute`/`list`/`data`'s `except` can't contain a `ref`
234/// at all per §7.1.1/§7.1.3/§7.1.4, so they need no special case here).
235fn collect_ref_edges<'a>(pattern: &'a Pattern, edges: &mut Vec<&'a str>) {
236    match pattern {
237        Pattern::Ref(name) => edges.push(name.as_str()),
238        Pattern::Group(values)
239        | Pattern::Interleave(values)
240        | Pattern::Choice(values)
241        | Pattern::OneOrMore(values)
242        // Present only pre-simplification — see collect_reachable_defines.
243        | Pattern::Optional(values)
244        | Pattern::ZeroOrMore(values)
245        | Pattern::Mixed(values) => {
246            for child in values {
247                collect_ref_edges(child, edges);
248            }
249        }
250        _ => {}
251    }
252}
253
254/// Flattens `items` (resolving `include`/`div`, see [`flatten_items`]),
255/// splits them into `start`/`define` fragments (see [`collect_items`]), and
256/// merges each name's fragments into one pattern per §4.17 (see [`merge`]).
257/// Shared by the top-level grammar and by a nested `grammar` pattern (see
258/// `simplify_pattern`'s `Pattern::Grammar` case) — `missing_start` is the
259/// error for a grammar with no `start` fragment at all, worded for whichever
260/// of the two calls this is.
261fn resolve_grammar_items(
262    items: &[GrammarItem],
263    resolver: &impl SchemaResolver,
264    loading: &mut BTreeSet<String>,
265    missing_start: &str,
266) -> Result<(Pattern, BTreeMap<String, Pattern>), SchemaError> {
267    let items = flatten_items(items, resolver, loading)?;
268    let (starts, definitions) = collect_items(items)?;
269    let start = merge("start", starts)?.ok_or_else(|| SchemaError::new(missing_start))?;
270    let definitions = definitions
271        .into_iter()
272        .map(|(name, fragments)| {
273            let pattern = merge(&name, fragments)?
274                .ok_or_else(|| SchemaError::new(format!("`{name}` has no pattern")))?;
275            Ok((name, pattern))
276        })
277        .collect::<Result<_, SchemaError>>()?;
278    Ok((start, definitions))
279}
280
281fn simplify_grammar(
282    grammar: &Grammar,
283    resolver: &impl SchemaResolver,
284    loading: &mut BTreeSet<String>,
285) -> Result<CompiledSchema, SchemaError> {
286    let (start, definitions) =
287        resolve_grammar_items(&grammar.items, resolver, loading, "grammar has no start")?;
288    // §4.19, checked here (pre-`notAllowed`-collapse) as well as again on
289    // the fully-simplified form in `simplify_root` — this pass sees
290    // `ref`s that §4.20 will later erase, but (unlike the raw form) can't
291    // see into a nested `grammar` pattern's own flattened/renamed defines,
292    // so neither check alone is a strict superset of the other.
293    check_illegal_recursion(&start, &definitions)?;
294    let mut extra = BTreeMap::new();
295    let mut next_id = 0usize;
296    let scope: BTreeSet<String> = definitions.keys().cloned().collect();
297    let start = simplify_pattern(
298        &start,
299        resolver,
300        loading,
301        &mut extra,
302        &mut next_id,
303        &scope,
304        None,
305    )?;
306    let mut compiled_definitions: BTreeMap<String, Pattern> = definitions
307        .into_iter()
308        .map(|(name, pattern)| {
309            Ok((
310                name,
311                simplify_pattern(
312                    &pattern,
313                    resolver,
314                    loading,
315                    &mut extra,
316                    &mut next_id,
317                    &scope,
318                    None,
319                )?,
320            ))
321        })
322        .collect::<Result<_, SchemaError>>()?;
323    compiled_definitions.extend(extra);
324    Ok(CompiledSchema {
325        start,
326        definitions: compiled_definitions,
327    })
328}
329
330/// One `start`'s or `define`'s worth of same-named fragments, each still
331/// paired with its own `combine` — the raw material [`merge`] combines into
332/// a single pattern per §4.17.
333type Fragments = Vec<(Option<Combine>, Vec<Pattern>)>;
334
335/// Collects the `start` fragments and `define` fragments (keyed by name) out
336/// of an already-flattened item list, preserving each fragment's own
337/// `combine` separately — consistency between fragments (§4.17) is checked
338/// uniformly by `merge`, the single source of truth for that rule. Shared
339/// by the top-level grammar and by nested `grammar` patterns (see
340/// `simplify_pattern`'s `Pattern::Grammar` case).
341fn collect_items(
342    items: Vec<GrammarItem>,
343) -> Result<(Fragments, BTreeMap<String, Fragments>), SchemaError> {
344    let mut starts = Vec::new();
345    let mut definitions: BTreeMap<String, Fragments> = BTreeMap::new();
346    for item in items {
347        match item {
348            GrammarItem::Start { combine, body } => starts.push((combine, body)),
349            GrammarItem::Define {
350                name,
351                combine,
352                body,
353            } => definitions.entry(name).or_default().push((combine, body)),
354            GrammarItem::Div(_) | GrammarItem::Include { .. } => {
355                unreachable!("items are flattened")
356            }
357        }
358    }
359    Ok((starts, definitions))
360}
361
362fn flatten_items(
363    items: &[GrammarItem],
364    resolver: &impl SchemaResolver,
365    loading: &mut BTreeSet<String>,
366) -> Result<Vec<GrammarItem>, SchemaError> {
367    let mut output = Vec::new();
368    for item in items {
369        match item {
370            GrammarItem::Div(children) => {
371                output.extend(flatten_items(children, resolver, loading)?)
372            }
373            GrammarItem::Include {
374                href,
375                body,
376                context,
377                ..
378            } => {
379                let key = format!("{}::{href}", context.base_uri);
380                if !loading.insert(key.clone()) {
381                    return Err(SchemaError::new("include cycle"));
382                }
383                let result = resolver
384                    .resolve(href, &context.base_uri)
385                    .map_err(|error| SchemaError::new(error.to_string()))
386                    .and_then(|source| {
387                        // §4.6/§4.7: this element's own `ns` transfers to
388                        // the referenced content as a fallback; likewise
389                        // (compact-syntax-specific, see
390                        // `parse_with_inherited_ns`'s doc comment) this
391                        // element's own compact-syntax `default namespace`.
392                        parse_with_inherited_ns(
393                            &source,
394                            context.ns.clone(),
395                            context.default_namespace.clone(),
396                        )
397                        .map_err(|error| SchemaError::new(error.to_string()))
398                    })
399                    .and_then(|included| {
400                        let Root::Grammar(grammar) = included.root else {
401                            return Err(SchemaError::new("include must resolve to a grammar"));
402                        };
403                        let mut included_items =
404                            flatten_items(&grammar.items, resolver, loading)?;
405                        let overriding_items = flatten_items(body, resolver, loading)?;
406                        check_include_overrides_exist(&included_items, &overriding_items)?;
407                        let local_names: BTreeSet<_> = overriding_items
408                            .iter()
409                            .filter_map(|item| match item {
410                                GrammarItem::Define { name, .. } => Some(name),
411                                _ => None,
412                            })
413                            .collect();
414                        included_items.retain(|item| !matches!(item, GrammarItem::Define { name, .. } if local_names.contains(name)));
415                        included_items.extend(overriding_items);
416                        Ok(included_items)
417                    });
418                loading.remove(&key);
419                output.extend(result?);
420            }
421            item => output.push(item.clone()),
422        }
423    }
424    Ok(output)
425}
426
427/// RELAX NG §4.7: an `include`'s override body may only override
428/// components the included grammar already has — a `start` override
429/// requires the included grammar to already declare a `start`; a
430/// `define` override requires a `define` of that exact name to already
431/// exist there. `included`/`overriding` are both already flattened
432/// (`Div`-transparent, matching how the rest of `include` resolution
433/// treats them).
434fn check_include_overrides_exist(
435    included: &[GrammarItem],
436    overriding: &[GrammarItem],
437) -> Result<(), SchemaError> {
438    let has_start = included
439        .iter()
440        .any(|item| matches!(item, GrammarItem::Start { .. }));
441    let define_names: BTreeSet<&String> = included
442        .iter()
443        .filter_map(|item| match item {
444            GrammarItem::Define { name, .. } => Some(name),
445            _ => None,
446        })
447        .collect();
448    for item in overriding {
449        match item {
450            GrammarItem::Start { .. } if !has_start => {
451                return Err(SchemaError::new(
452                    "RELAX NG §4.7: include overrides `start`, but the included grammar has none",
453                ));
454            }
455            GrammarItem::Define { name, .. } if !define_names.contains(name) => {
456                return Err(SchemaError::new(format!(
457                    "RELAX NG §4.7: include overrides `define name=\"{name}\"`, but the included grammar has no such define"
458                )));
459            }
460            _ => {}
461        }
462    }
463    Ok(())
464}
465
466fn merge(
467    name: &str,
468    fragments: Vec<(Option<Combine>, Vec<Pattern>)>,
469) -> Result<Option<Pattern>, SchemaError> {
470    let mut combine = None;
471    let mut without_combine = 0usize;
472    let mut patterns = Vec::new();
473    for (method, bodies) in fragments {
474        // §4.17: at most one fragment may omit `combine` — the rest must
475        // specify it, and agree when they do. (A *single* fragment lacking
476        // `combine`, i.e. no duplicates at all, is of course fine.)
477        match method {
478            Some(method) => {
479                if combine.is_some_and(|existing| existing != method) {
480                    return Err(SchemaError::new(format!(
481                        "`{name}` has inconsistent combine"
482                    )));
483                }
484                combine = Some(method);
485            }
486            None => without_combine += 1,
487        }
488        patterns.extend(bodies);
489    }
490    if without_combine > 1 {
491        return Err(SchemaError::new(format!(
492            "`{name}` has more than one definition without combine"
493        )));
494    }
495    if patterns.is_empty() {
496        return Ok(None);
497    }
498    if patterns.len() == 1 {
499        return Ok(Some(patterns.pop().expect("one pattern")));
500    }
501    match combine {
502        Some(Combine::Choice) => Ok(Some(Pattern::Choice(patterns))),
503        Some(Combine::Interleave) => Ok(Some(Pattern::Interleave(patterns))),
504        None => Err(SchemaError::new(format!(
505            "duplicate `{name}` without combine"
506        ))),
507    }
508}
509
510fn simplify_pattern(
511    pattern: &Pattern,
512    resolver: &impl SchemaResolver,
513    loading: &mut BTreeSet<String>,
514    extra: &mut BTreeMap<String, Pattern>,
515    next_id: &mut usize,
516    // §4.18: valid `ref`/`parentRef` targets at this point in the tree —
517    // checked *here*, during the normal bottom-up recursion, rather than
518    // on the fully-simplified `compiled.definitions` afterwards, so an
519    // undefined `ref` is still caught even if it sits inside a branch that
520    // `notAllowed`-propagation (§4.20) will later collapse away. `scope`
521    // is `ref`'s namespace (the nearest enclosing grammar's own defines);
522    // `parent_scope` is `parentRef`'s (the grammar enclosing *that one*).
523    scope: &BTreeSet<String>,
524    parent_scope: Option<&BTreeSet<String>>,
525) -> Result<Pattern, SchemaError> {
526    let mut children = |patterns: &[Pattern]| {
527        patterns
528            .iter()
529            .map(|child| {
530                simplify_pattern(
531                    child,
532                    resolver,
533                    loading,
534                    extra,
535                    next_id,
536                    scope,
537                    parent_scope,
538                )
539            })
540            .collect::<Result<Vec<_>, _>>()
541    };
542    Ok(match pattern {
543        Pattern::Ref(name) => {
544            if !scope.contains(name) {
545                return Err(SchemaError::new(format!(
546                    "RELAX NG §4.18: `ref` to undefined `{name}`"
547                )));
548            }
549            pattern.clone()
550        }
551        Pattern::ParentRef(name) => {
552            if !parent_scope.is_some_and(|names| names.contains(name)) {
553                return Err(SchemaError::new(format!(
554                    "RELAX NG §4.18: `parentRef` to undefined `{name}`"
555                )));
556            }
557            pattern.clone()
558        }
559        Pattern::Optional(values) => choice(vec![group(children(values)?), Pattern::Empty]),
560        Pattern::ZeroOrMore(values) => {
561            let repeated = one_or_more(group(children(values)?));
562            choice(vec![repeated, Pattern::Empty])
563        }
564        Pattern::Mixed(values) => interleave(vec![group(children(values)?), Pattern::Text]),
565        Pattern::OneOrMore(values) => one_or_more(group(children(values)?)),
566        Pattern::List(values) => wrap_unless_not_allowed(group(children(values)?), Pattern::List),
567        Pattern::Group(values) => group(children(values)?),
568        Pattern::Choice(values) => choice(children(values)?),
569        Pattern::Interleave(values) => interleave(children(values)?),
570        Pattern::Element {
571            name,
572            body,
573            context,
574        } => {
575            check_name_class_constraints(name)?;
576            Pattern::Element {
577                name: name.clone(),
578                body: vec![group(children(body)?)],
579                context: context.clone(),
580            }
581        }
582        Pattern::Attribute {
583            name,
584            body,
585            context,
586        } => {
587            check_name_class_constraints(name)?;
588            check_attribute_name_class_constraints(name)?;
589            wrap_unless_not_allowed(group(children(body)?), |body| Pattern::Attribute {
590                name: name.clone(),
591                body,
592                context: context.clone(),
593            })
594        }
595        Pattern::Data {
596            datatype,
597            datatype_library,
598            params,
599            except,
600            context,
601        } => {
602            check_builtin_datatype_constraints(datatype, datatype_library, !params.is_empty())?;
603            Pattern::Data {
604                datatype: datatype.clone(),
605                datatype_library: datatype_library.clone(),
606                params: params.clone(),
607                // §4.20: an `except` with a `notAllowed` child is removed —
608                // excluding "no value" excludes nothing.
609                except: children(except)?
610                    .into_iter()
611                    .filter(|value| !matches!(value, Pattern::NotAllowed))
612                    .collect(),
613                context: context.clone(),
614            }
615        }
616        Pattern::Value {
617            datatype: Some(datatype),
618            datatype_library,
619            ..
620        } => {
621            check_builtin_datatype_constraints(datatype, datatype_library, false)?;
622            pattern.clone()
623        }
624        Pattern::ExternalRef { href, context, .. } => {
625            let key = format!("{}::{href}", context.base_uri);
626            if !loading.insert(key.clone()) {
627                return Err(SchemaError::new("externalRef cycle"));
628            }
629            // `key` must stay marked "loading" for the *entire* recursive
630            // simplification below, not just while resolving/parsing —
631            // otherwise a schema that externalRefs right back to itself
632            // never re-hits the cycle check and recurses forever. Compute
633            // the result without an early `?` so `loading.remove` always
634            // runs first, success or failure, mirroring `Include`'s
635            // (already-correct) handling above.
636            let result = resolver
637                .resolve(href, &context.base_uri)
638                .map_err(|error| SchemaError::new(error.to_string()))
639                .and_then(|source| {
640                    // §4.6: this element's own `ns` transfers to the
641                    // referenced content as a fallback; likewise
642                    // (compact-syntax-specific, see
643                    // `parse_with_inherited_ns`'s doc comment) this
644                    // element's own compact-syntax `default namespace`.
645                    parse_with_inherited_ns(
646                        &source,
647                        context.ns.clone(),
648                        context.default_namespace.clone(),
649                    )
650                    .map_err(|error| SchemaError::new(error.to_string()))
651                })
652                .and_then(|resolved| match resolved.root {
653                    Root::Pattern(value) => simplify_pattern(
654                        &value,
655                        resolver,
656                        loading,
657                        extra,
658                        next_id,
659                        &BTreeSet::new(),
660                        None,
661                    ),
662                    Root::Grammar(_) => {
663                        Err(SchemaError::new("externalRef must resolve to a pattern"))
664                    }
665                });
666            loading.remove(&key);
667            result?
668        }
669        Pattern::Grammar(nested) => {
670            let (start_body, raw_defines) = resolve_grammar_items(
671                &nested.items,
672                resolver,
673                loading,
674                "nested grammar has no start",
675            )?;
676            let nested_scope: BTreeSet<String> = raw_defines.keys().cloned().collect();
677
678            let simplified_start = simplify_pattern(
679                &start_body,
680                resolver,
681                loading,
682                extra,
683                next_id,
684                &nested_scope,
685                Some(scope),
686            )?;
687            let mut simplified_defines = BTreeMap::new();
688            for (name, body) in &raw_defines {
689                simplified_defines.insert(
690                    name.clone(),
691                    simplify_pattern(
692                        body,
693                        resolver,
694                        loading,
695                        extra,
696                        next_id,
697                        &nested_scope,
698                        Some(scope),
699                    )?,
700                );
701            }
702
703            // §4.19: a `grammar` occurring in pattern position (rather than
704            // as the schema root) is flattened away — its `define`s move up
705            // into the surrounding definition table under fresh names, and
706            // its `ref`s are rewritten to match. `parentRef` becomes a plain
707            // `ref` into the *enclosing* scope, left for that scope's own
708            // flattening pass (or the top-level table) to resolve.
709            *next_id += 1;
710            let tag = *next_id;
711            let rename: BTreeMap<String, String> = raw_defines
712                .keys()
713                .map(|name| (name.clone(), format!("{name}#{tag}")))
714                .collect();
715            for (name, pattern) in simplified_defines {
716                extra.insert(rename[&name].clone(), rewrite_refs(&pattern, &rename));
717            }
718            rewrite_refs(&simplified_start, &rename)
719        }
720        value => value.clone(),
721    })
722}
723
724/// Rewrites `ref`/`parentRef` occurrences after a nested `grammar` pattern
725/// has been flattened: local `ref`s follow the rename map into their fresh
726/// name, and `parentRef`s become plain `ref`s into the enclosing scope.
727fn rewrite_refs(pattern: &Pattern, rename: &BTreeMap<String, String>) -> Pattern {
728    let rewrite_all = |values: &[Pattern]| -> Vec<Pattern> {
729        values
730            .iter()
731            .map(|value| rewrite_refs(value, rename))
732            .collect()
733    };
734    match pattern {
735        Pattern::Ref(name) => {
736            Pattern::Ref(rename.get(name).cloned().unwrap_or_else(|| name.clone()))
737        }
738        Pattern::ParentRef(name) => Pattern::Ref(name.clone()),
739        Pattern::Element {
740            name,
741            body,
742            context,
743        } => Pattern::Element {
744            name: name.clone(),
745            body: rewrite_all(body),
746            context: context.clone(),
747        },
748        Pattern::Attribute {
749            name,
750            body,
751            context,
752        } => Pattern::Attribute {
753            name: name.clone(),
754            body: rewrite_all(body),
755            context: context.clone(),
756        },
757        Pattern::Group(values) => Pattern::Group(rewrite_all(values)),
758        Pattern::Interleave(values) => Pattern::Interleave(rewrite_all(values)),
759        Pattern::Choice(values) => Pattern::Choice(rewrite_all(values)),
760        Pattern::Optional(values) => Pattern::Optional(rewrite_all(values)),
761        Pattern::ZeroOrMore(values) => Pattern::ZeroOrMore(rewrite_all(values)),
762        Pattern::OneOrMore(values) => Pattern::OneOrMore(rewrite_all(values)),
763        Pattern::List(values) => Pattern::List(rewrite_all(values)),
764        Pattern::Mixed(values) => Pattern::Mixed(rewrite_all(values)),
765        Pattern::Data {
766            datatype,
767            datatype_library,
768            params,
769            except,
770            context,
771        } => Pattern::Data {
772            datatype: datatype.clone(),
773            datatype_library: datatype_library.clone(),
774            params: params.clone(),
775            except: rewrite_all(except),
776            context: context.clone(),
777        },
778        Pattern::Grammar(_) => unreachable!("nested grammars are flattened before rewriting refs"),
779        value => value.clone(),
780    }
781}
782
783/// `empty` is the identity element for `group`/`interleave` composition
784/// (`p,empty ≡ p`, `p&empty ≡ p`) — drop it so a formerly-multi-child
785/// group/interleave collapses down to its one remaining real operand
786/// instead of staying wrapped around it. Safe now that both `<start>`
787/// (§4.12) and the `oneOrMore//group//attribute` family of §7.1
788/// restrictions no longer depend on a `group`/`interleave` node
789/// surviving purely because an `empty` sibling kept its child count above
790/// one — see plan/00-STATUS.md for the case that used to make this unsafe.
791fn drop_identity_empty(mut values: Vec<Pattern>) -> Vec<Pattern> {
792    if values.len() > 1 {
793        values.retain(|value| !matches!(value, Pattern::Empty));
794    }
795    values
796}
797
798/// Builds an already-`Vec`-simplification-normalized `group`/`interleave`:
799/// any `notAllowed` operand collapses the whole thing to `notAllowed`
800/// (`p,notAllowed ≡ notAllowed`, and likewise for `&`), then a leftover
801/// single operand (after dropping `empty` identities) stands in for the
802/// wrapper itself. Shared by [`group`]/[`interleave`], which only differ in
803/// which `Pattern` variant they wrap multiple leftover operands in — also
804/// used by the validator (Phase 05) to keep derivative-produced patterns
805/// canonical/small the same way simplification does.
806fn collapse_sequence(values: Vec<Pattern>, wrap: fn(Vec<Pattern>) -> Pattern) -> Pattern {
807    if values
808        .iter()
809        .any(|value| matches!(value, Pattern::NotAllowed))
810    {
811        return Pattern::NotAllowed;
812    }
813    let mut values = drop_identity_empty(values);
814    if values.is_empty() {
815        Pattern::Empty
816    } else if values.len() == 1 {
817        values.pop().expect("one")
818    } else {
819        wrap(values)
820    }
821}
822pub(crate) fn group(values: Vec<Pattern>) -> Pattern {
823    collapse_sequence(values, Pattern::Group)
824}
825pub(crate) fn choice(mut values: Vec<Pattern>) -> Pattern {
826    values.retain(|value| !matches!(value, Pattern::NotAllowed));
827    if values.is_empty() {
828        Pattern::NotAllowed
829    } else if values.len() == 1 {
830        values.pop().expect("one")
831    } else {
832        Pattern::Choice(values)
833    }
834}
835pub(crate) fn interleave(values: Vec<Pattern>) -> Pattern {
836    collapse_sequence(values, Pattern::Interleave)
837}
838/// `notAllowed` absorbs any wrapper built around it (`X(notAllowed) ≡
839/// notAllowed` for every single-body `X`) — otherwise wraps `body` in a
840/// one-element `Vec` via `wrap`. Shared by [`one_or_more`] and
841/// `simplify_pattern`'s `List`/`Attribute` cases.
842fn wrap_unless_not_allowed(body: Pattern, wrap: impl FnOnce(Vec<Pattern>) -> Pattern) -> Pattern {
843    if matches!(body, Pattern::NotAllowed) {
844        Pattern::NotAllowed
845    } else {
846        wrap(vec![body])
847    }
848}
849
850pub(crate) fn one_or_more(body: Pattern) -> Pattern {
851    wrap_unless_not_allowed(body, Pattern::OneOrMore)
852}
853
854/// Shared traversal state for [`check_restrictions`], bundled into one
855/// struct purely to keep that function's argument count reasonable.
856struct RestrictionScope<'a> {
857    definitions: &'a BTreeMap<String, Pattern>,
858    // `ref` is transparent for these restrictions — a schema is free to
859    // put e.g. the `oneOrMore` that §7.3 requires around a `ref` instead of
860    // inside the `define` it points to (this is exactly how validation
861    // itself treats `ref`, and real-world schemas rely on it). `visiting`
862    // guards against re-entering a `ref` already being expanded on the
863    // current path; §4.19 (checked earlier) already guarantees such a
864    // cycle can only happen through an `element`, which resets every flag
865    // here to `false` anyway, so this is a defensive backstop rather than
866    // something expected to trigger.
867    visiting: BTreeSet<String>,
868    // A `define` that's shared by many `ref`s (common in real-world
869    // schemas, e.g. an attribute group referenced from hundreds of
870    // elements) would otherwise get re-walked once per occurrence, and
871    // since occurrences nest, that's exponential in schema size. Once a
872    // `(name, flags)` combination has passed, it always will (the check is
873    // a pure function of the definition's structure and these flags), so
874    // it's cached and skipped on repeat.
875    verified: BTreeSet<(String, bool, bool, bool, bool)>,
876}
877
878fn check_restrictions(
879    pattern: &Pattern,
880    in_attribute: bool,
881    in_list: bool,
882    in_one_or_more: bool,
883    // §7.1.2: descendant of a `group`/`interleave` that is itself a
884    // descendant of a `oneOrMore` (not the same as `in_one_or_more` alone —
885    // `oneOrMore { attribute a }` is fine, only wrapping a group/interleave
886    // that mixes in an attribute is banned).
887    in_one_or_more_container: bool,
888    scope: &mut RestrictionScope,
889) -> Result<(), SchemaError> {
890    match pattern {
891        Pattern::Ref(name) => {
892            let key = (
893                name.clone(),
894                in_attribute,
895                in_list,
896                in_one_or_more,
897                in_one_or_more_container,
898            );
899            if scope.verified.contains(&key) {
900                return Ok(());
901            }
902            if let Some(definition) = scope.definitions.get(name)
903                && scope.visiting.insert(name.clone())
904            {
905                check_restrictions(
906                    definition,
907                    in_attribute,
908                    in_list,
909                    in_one_or_more,
910                    in_one_or_more_container,
911                    scope,
912                )?;
913                scope.visiting.remove(name);
914                scope.verified.insert(key);
915            }
916        }
917        Pattern::Attribute { name, body, .. } => {
918            if in_list {
919                return Err(SchemaError::new(
920                    "RELAX NG §7.1.3: a list pattern cannot contain attribute",
921                ));
922            }
923            if in_attribute {
924                return Err(SchemaError::new(
925                    "RELAX NG §7.1.1: an attribute pattern cannot contain an attribute",
926                ));
927            }
928            if in_one_or_more_container {
929                return Err(SchemaError::new(
930                    "RELAX NG §7.1.2: a oneOrMore pattern cannot wrap a group/interleave containing an attribute",
931                ));
932            }
933            if name_class_has_infinite_names(name) && !in_one_or_more {
934                return Err(SchemaError::new(
935                    "RELAX NG §7.3: an attribute with an anyName or nsName name class must have a oneOrMore ancestor",
936                ));
937            }
938            body.iter().try_for_each(|child| {
939                check_restrictions(
940                    child,
941                    true,
942                    in_list,
943                    in_one_or_more,
944                    in_one_or_more_container,
945                    scope,
946                )
947            })?;
948        }
949        Pattern::List(body) => {
950            if in_list {
951                return Err(SchemaError::new(
952                    "RELAX NG §7.1.3: a list pattern cannot contain list",
953                ));
954            }
955            body.iter().try_for_each(|child| {
956                check_restrictions(
957                    child,
958                    in_attribute,
959                    true,
960                    in_one_or_more,
961                    in_one_or_more_container,
962                    scope,
963                )
964            })?;
965        }
966        Pattern::Interleave(values) if in_list => {
967            return Err(SchemaError::new(
968                "RELAX NG §7.1.3: a list pattern cannot contain interleave",
969            ));
970        }
971        Pattern::Text if in_list => {
972            return Err(SchemaError::new(
973                "RELAX NG §7.1.3: a list pattern cannot contain text",
974            ));
975        }
976        Pattern::Element { .. } if in_attribute => {
977            return Err(SchemaError::new(
978                "RELAX NG §7.1.1: an attribute pattern cannot contain an element",
979            ));
980        }
981        Pattern::Element { .. } if in_list => {
982            return Err(SchemaError::new(
983                "RELAX NG §7.1.3: a list pattern cannot contain element",
984            ));
985        }
986        // `p*` desugars to `choice(empty, oneOrMore(p))` (§4.15), so for
987        // restriction purposes it grants the same oneOrMore ancestor as a
988        // literal `oneOrMore` — real-world schemas commonly write `attr*`
989        // rather than `(attr)+` for a repeatable wildcard attribute.
990        Pattern::OneOrMore(values) | Pattern::ZeroOrMore(values) => {
991            values.iter().try_for_each(|child| {
992                check_restrictions(
993                    child,
994                    in_attribute,
995                    in_list,
996                    true,
997                    in_one_or_more_container,
998                    scope,
999                )
1000            })?;
1001        }
1002        Pattern::Group(values) | Pattern::Interleave(values) => {
1003            let in_container = in_one_or_more_container || in_one_or_more;
1004            values.iter().try_for_each(|child| {
1005                check_restrictions(
1006                    child,
1007                    in_attribute,
1008                    in_list,
1009                    in_one_or_more,
1010                    in_container,
1011                    scope,
1012                )
1013            })?;
1014        }
1015        Pattern::Choice(values) | Pattern::Optional(values) | Pattern::Mixed(values) => {
1016            values.iter().try_for_each(|child| {
1017                check_restrictions(
1018                    child,
1019                    in_attribute,
1020                    in_list,
1021                    in_one_or_more,
1022                    in_one_or_more_container,
1023                    scope,
1024                )
1025            })?;
1026        }
1027        Pattern::Element { body, .. } => {
1028            body.iter().try_for_each(|child| {
1029                check_restrictions(child, false, false, false, false, scope)
1030            })?;
1031        }
1032        Pattern::Data { except, .. } => {
1033            except.iter().try_for_each(check_except_restrictions)?;
1034        }
1035        _ => {}
1036    }
1037    Ok(())
1038}
1039
1040/// RELAX NG §4.16: a `data`/`value` must use a datatype known to its
1041/// library. We don't implement datatype libraries (Phase 06) or
1042/// `datatypeLibrary` inheritance (§4.3) yet, but the built-in library
1043/// (`datatypeLibrary=""`, explicitly) is part of the spec itself: it
1044/// defines exactly `string` and `token`, neither taking any parameters.
1045/// Only checked when `datatypeLibrary=""` is explicit — an *absent*
1046/// attribute may still inherit a real library from an ancestor, which
1047/// we can't yet resolve, so treating it as empty would misfire.
1048fn check_builtin_datatype_constraints(
1049    datatype: &str,
1050    datatype_library: &Option<String>,
1051    has_params: bool,
1052) -> Result<(), SchemaError> {
1053    if datatype_library.as_deref() != Some("") {
1054        return Ok(());
1055    }
1056    if datatype != "string" && datatype != "token" {
1057        return Err(SchemaError::new(format!(
1058            "RELAX NG §4.16: `{datatype}` is not a datatype of the built-in (empty) datatype library"
1059        )));
1060    }
1061    if has_params {
1062        return Err(SchemaError::new(
1063            "RELAX NG §4.16: the built-in (empty) datatype library's datatypes take no parameters",
1064        ));
1065    }
1066    Ok(())
1067}
1068
1069/// RELAX NG §7.1.4: a `data/except` may only contain `data`, `value` or
1070/// `choice` (recursively) — not `attribute`, `ref`, `text`, `list`,
1071/// `group`, `interleave`, `oneOrMore` or `empty`.
1072fn check_except_restrictions(pattern: &Pattern) -> Result<(), SchemaError> {
1073    match pattern {
1074        Pattern::Data { except, .. } => except.iter().try_for_each(check_except_restrictions),
1075        Pattern::Value { .. } => Ok(()),
1076        Pattern::Choice(values) => values.iter().try_for_each(check_except_restrictions),
1077        _ => Err(SchemaError::new(
1078            "RELAX NG §7.1.4: a data/except pattern may only contain data, value or choice",
1079        )),
1080    }
1081}
1082
1083/// RELAX NG §7.1.5: `start` may only (transitively, without crossing an
1084/// `element`) be composed of `choice` and `ref` around an `element` (or
1085/// `notAllowed`) — not `attribute`, `data`, `value`, `text`, `list`,
1086/// `group`, `interleave`, `oneOrMore` or `empty`.
1087fn check_start_restrictions(
1088    pattern: &Pattern,
1089    definitions: &BTreeMap<String, Pattern>,
1090    visiting: &mut BTreeSet<String>,
1091) -> Result<(), SchemaError> {
1092    match pattern {
1093        Pattern::Element { .. } | Pattern::NotAllowed => Ok(()),
1094        Pattern::Choice(values) => values
1095            .iter()
1096            .try_for_each(|child| check_start_restrictions(child, definitions, visiting)),
1097        Pattern::Ref(name) => {
1098            if let Some(definition) = definitions.get(name)
1099                && visiting.insert(name.clone())
1100            {
1101                check_start_restrictions(definition, definitions, visiting)?;
1102                visiting.remove(name);
1103            }
1104            Ok(())
1105        }
1106        Pattern::Attribute { .. } => Err(SchemaError::new(
1107            "RELAX NG §7.1.5: start cannot contain attribute",
1108        )),
1109        Pattern::Data { .. } => Err(SchemaError::new(
1110            "RELAX NG §7.1.5: start cannot contain data",
1111        )),
1112        Pattern::Value { .. } => Err(SchemaError::new(
1113            "RELAX NG §7.1.5: start cannot contain value",
1114        )),
1115        Pattern::Text => Err(SchemaError::new(
1116            "RELAX NG §7.1.5: start cannot contain text",
1117        )),
1118        Pattern::List(_) => Err(SchemaError::new(
1119            "RELAX NG §7.1.5: start cannot contain list",
1120        )),
1121        Pattern::Group(_) => Err(SchemaError::new(
1122            "RELAX NG §7.1.5: start cannot contain group",
1123        )),
1124        Pattern::Interleave(_) => Err(SchemaError::new(
1125            "RELAX NG §7.1.5: start cannot contain interleave",
1126        )),
1127        Pattern::OneOrMore(_) => Err(SchemaError::new(
1128            "RELAX NG §7.1.5: start cannot contain oneOrMore",
1129        )),
1130        Pattern::Empty => Err(SchemaError::new(
1131            "RELAX NG §7.1.5: start cannot contain empty",
1132        )),
1133        Pattern::Optional(_)
1134        | Pattern::ZeroOrMore(_)
1135        | Pattern::Mixed(_)
1136        | Pattern::ExternalRef { .. }
1137        | Pattern::ParentRef(_)
1138        | Pattern::Grammar(_) => {
1139            unreachable!("resolved into other patterns by simplify_pattern")
1140        }
1141    }
1142}
1143
1144/// RELAX NG §4.16: an `anyName`'s `except` must not have any `anyName`
1145/// descendant; an `nsName`'s `except` must not have any `nsName` or
1146/// `anyName` descendant. Checked independently of ref resolution/
1147/// reachability, so it runs during the normal top-down walk before any
1148/// dead branch could be collapsed away.
1149fn check_name_class_constraints(class: &NameClass) -> Result<(), SchemaError> {
1150    match class {
1151        NameClass::Any { except, .. } => {
1152            for item in except {
1153                if name_class_contains_any(item) {
1154                    return Err(SchemaError::new(
1155                        "RELAX NG §4.16: an anyName's except must not contain anyName",
1156                    ));
1157                }
1158                check_name_class_constraints(item)?;
1159            }
1160            Ok(())
1161        }
1162        NameClass::Namespace { except, .. } => {
1163            for item in except {
1164                if name_class_contains_any(item) || name_class_contains_namespace(item) {
1165                    return Err(SchemaError::new(
1166                        "RELAX NG §4.16: an nsName's except must not contain nsName or anyName",
1167                    ));
1168                }
1169                check_name_class_constraints(item)?;
1170            }
1171            Ok(())
1172        }
1173        NameClass::Name { .. } => Ok(()),
1174        NameClass::Choice(options) => {
1175            for item in options {
1176                check_name_class_constraints(item)?;
1177            }
1178            Ok(())
1179        }
1180    }
1181}
1182
1183const XMLNS_NAMESPACE: &str = "http://www.w3.org/2000/xmlns";
1184
1185/// The spec's own wording gives this URI without a trailing slash, but the
1186/// well-known XML Namespaces URI for `xmlns` has one — accept either.
1187fn is_xmlns_namespace(namespace: &Option<String>) -> bool {
1188    namespace
1189        .as_deref()
1190        .is_some_and(|value| value.trim_end_matches('/') == XMLNS_NAMESPACE)
1191}
1192
1193/// RELAX NG §4.16: within an `attribute`'s name class, `xmlns` (in no
1194/// namespace) and the `http://www.w3.org/2000/xmlns` namespace are
1195/// reserved — an attribute pattern must not be able to match either, since
1196/// neither denotes a real XML attribute.
1197fn check_attribute_name_class_constraints(class: &NameClass) -> Result<(), SchemaError> {
1198    match class {
1199        NameClass::Name {
1200            lexical, namespace, ..
1201        } => {
1202            if ns_eq(namespace, &None) && lexical == "xmlns" {
1203                return Err(SchemaError::new(
1204                    "RELAX NG §4.16: an attribute name class must not match `xmlns` in no namespace",
1205                ));
1206            }
1207            if is_xmlns_namespace(namespace) {
1208                return Err(SchemaError::new(format!(
1209                    "RELAX NG §4.16: an attribute name class must not use the {XMLNS_NAMESPACE} namespace"
1210                )));
1211            }
1212            Ok(())
1213        }
1214        NameClass::Namespace {
1215            namespace, except, ..
1216        } => {
1217            if is_xmlns_namespace(namespace) {
1218                return Err(SchemaError::new(format!(
1219                    "RELAX NG §4.16: an attribute name class must not use the {XMLNS_NAMESPACE} namespace"
1220                )));
1221            }
1222            except
1223                .iter()
1224                .try_for_each(check_attribute_name_class_constraints)
1225        }
1226        NameClass::Any { except, .. } => except
1227            .iter()
1228            .try_for_each(check_attribute_name_class_constraints),
1229        NameClass::Choice(options) => options
1230            .iter()
1231            .try_for_each(check_attribute_name_class_constraints),
1232    }
1233}
1234
1235/// Whether `class` (through any `except`/`choice` descendant) contains a
1236/// name-class atom `is_target` accepts — shared by
1237/// `name_class_contains_any`/`name_class_contains_namespace`, which only
1238/// differ in which atom kind they're looking for.
1239fn name_class_contains(class: &NameClass, is_target: fn(&NameClass) -> bool) -> bool {
1240    if is_target(class) {
1241        return true;
1242    }
1243    match class {
1244        NameClass::Name { .. } => false,
1245        NameClass::Any { except, .. } | NameClass::Namespace { except, .. } => except
1246            .iter()
1247            .any(|item| name_class_contains(item, is_target)),
1248        NameClass::Choice(options) => options
1249            .iter()
1250            .any(|item| name_class_contains(item, is_target)),
1251    }
1252}
1253
1254fn name_class_contains_any(class: &NameClass) -> bool {
1255    name_class_contains(class, |class| matches!(class, NameClass::Any { .. }))
1256}
1257
1258fn name_class_contains_namespace(class: &NameClass) -> bool {
1259    name_class_contains(class, |class| matches!(class, NameClass::Namespace { .. }))
1260}
1261
1262/// True if `class` (through any `choice` branch) is an `anyName` or
1263/// `nsName` — i.e. can match more than one distinct attribute name.
1264fn name_class_has_infinite_names(class: &NameClass) -> bool {
1265    match class {
1266        NameClass::Name { .. } => false,
1267        NameClass::Any { .. } | NameClass::Namespace { .. } => true,
1268        NameClass::Choice(options) => options.iter().any(name_class_has_infinite_names),
1269    }
1270}
1271
1272/// Flattens `choice` name classes into their leaf (`name`/`nsName`/`anyName`)
1273/// alternatives.
1274fn name_class_atoms(class: &NameClass) -> Vec<&NameClass> {
1275    match class {
1276        NameClass::Choice(options) => options.iter().flat_map(name_class_atoms).collect(),
1277        atom => vec![atom],
1278    }
1279}
1280
1281/// `None` (no explicit `ns` attribute on a `name`/`nsName`, e.g. inside an
1282/// `attribute`, which — unlike `element` — never inherits a default
1283/// namespace) and `Some("")` both denote "no namespace"; treat them as
1284/// equal rather than as two different namespaces.
1285pub(crate) fn ns_eq(a: &Option<String>, b: &Option<String>) -> bool {
1286    a.as_deref().unwrap_or("") == b.as_deref().unwrap_or("")
1287}
1288
1289/// Whether `class` matches the given expanded name. Shared with the
1290/// validator (Phase 05), which matches document element/attribute names
1291/// against schema name classes the same way the §7.3/§7.4 restriction
1292/// checks match schema name classes against each other.
1293pub(crate) fn name_class_matches(
1294    class: &NameClass,
1295    namespace: &Option<String>,
1296    local: &str,
1297) -> bool {
1298    match class {
1299        NameClass::Name {
1300            lexical,
1301            namespace: ns,
1302            ..
1303        } => ns_eq(ns, namespace) && lexical == local,
1304        NameClass::Any { except, .. } => !except
1305            .iter()
1306            .any(|item| name_class_matches(item, namespace, local)),
1307        NameClass::Namespace {
1308            namespace: ns,
1309            except,
1310            ..
1311        } => {
1312            ns_eq(ns, namespace)
1313                && !except
1314                    .iter()
1315                    .any(|item| name_class_matches(item, namespace, local))
1316        }
1317        NameClass::Choice(options) => options
1318            .iter()
1319            .any(|item| name_class_matches(item, namespace, local)),
1320    }
1321}
1322
1323/// An `except` list is finite, so it can only ever exclude finitely many
1324/// local names from an (unbounded) namespace — unless one of its own
1325/// alternatives is itself an unrestricted `anyName`, which denotes "every
1326/// name" and so excludes everything.
1327fn excludes_everything(except: &[NameClass]) -> bool {
1328    except.iter().any(|item| {
1329        name_class_atoms(item)
1330            .into_iter()
1331            .any(|atom| matches!(atom, NameClass::Any { except, .. } if except.is_empty()))
1332    })
1333}
1334
1335fn name_class_atom_overlaps(a: &NameClass, b: &NameClass) -> bool {
1336    match (a, b) {
1337        (
1338            NameClass::Name {
1339                lexical: l1,
1340                namespace: n1,
1341                ..
1342            },
1343            NameClass::Name {
1344                lexical: l2,
1345                namespace: n2,
1346                ..
1347            },
1348        ) => l1 == l2 && ns_eq(n1, n2),
1349        (
1350            NameClass::Name {
1351                lexical, namespace, ..
1352            },
1353            other,
1354        )
1355        | (
1356            other,
1357            NameClass::Name {
1358                lexical, namespace, ..
1359            },
1360        ) => name_class_matches(other, namespace, lexical),
1361        (
1362            NameClass::Namespace {
1363                namespace: n1,
1364                except: e1,
1365                ..
1366            },
1367            NameClass::Namespace {
1368                namespace: n2,
1369                except: e2,
1370                ..
1371            },
1372        ) => ns_eq(n1, n2) && !excludes_everything(e1) && !excludes_everything(e2),
1373        (NameClass::Namespace { except, .. }, NameClass::Any { except: other, .. })
1374        | (NameClass::Any { except: other, .. }, NameClass::Namespace { except, .. }) => {
1375            !excludes_everything(except) && !excludes_everything(other)
1376        }
1377        (NameClass::Any { except: e1, .. }, NameClass::Any { except: e2, .. }) => {
1378            !excludes_everything(e1) && !excludes_everything(e2)
1379        }
1380        (NameClass::Choice(_), _) | (_, NameClass::Choice(_)) => {
1381            unreachable!("choices are flattened into atoms before comparison")
1382        }
1383    }
1384}
1385
1386/// Whether two name classes admit at least one common expanded name.
1387fn name_classes_overlap(a: &NameClass, b: &NameClass) -> bool {
1388    let atoms_b = name_class_atoms(b);
1389    name_class_atoms(a)
1390        .into_iter()
1391        .any(|x| atoms_b.iter().any(|y| name_class_atom_overlaps(x, y)))
1392}
1393
1394/// What a `group`/`interleave` operand structurally contains, gathered
1395/// transparently through everything except `element`/`attribute` bodies
1396/// (those start a fresh scope) and `list` (excluded per §7.2) — resolving
1397/// `ref`s (with cycle protection, since legal recursive grammars exist)
1398/// since restrictions apply just as much to factored-out definitions as to
1399/// inline patterns.
1400#[derive(Default)]
1401struct Occurrences<'a> {
1402    attributes: Vec<&'a NameClass>,
1403    elements: Vec<&'a NameClass>,
1404    has_text: bool,
1405    /// §7.2: can this operand produce an element, data, value, list or
1406    /// text node — i.e. "match a child"?
1407    child_capable: bool,
1408    /// §7.2: can this operand produce a data, value or list node — i.e.
1409    /// "match a single string"?
1410    string_capable: bool,
1411}
1412
1413fn collect_occurrences<'a>(
1414    pattern: &'a Pattern,
1415    definitions: &'a BTreeMap<String, Pattern>,
1416    visiting: &mut BTreeSet<String>,
1417    out: &mut Occurrences<'a>,
1418) {
1419    match pattern {
1420        Pattern::Attribute { name, .. } => out.attributes.push(name),
1421        Pattern::Element { name, .. } => {
1422            out.elements.push(name);
1423            out.child_capable = true;
1424        }
1425        Pattern::Text => {
1426            out.has_text = true;
1427            out.child_capable = true;
1428        }
1429        Pattern::Data { .. } | Pattern::Value { .. } => {
1430            out.child_capable = true;
1431            out.string_capable = true;
1432        }
1433        Pattern::List(_) => {
1434            // §7.2 explicitly excludes patterns *within* list, but list
1435            // itself is a leaf that matches a (single-string) child.
1436            out.child_capable = true;
1437            out.string_capable = true;
1438        }
1439        Pattern::Ref(target) => {
1440            if let Some(definition) = definitions.get(target)
1441                && visiting.insert(target.clone())
1442            {
1443                collect_occurrences(definition, definitions, visiting, out);
1444                visiting.remove(target);
1445            }
1446        }
1447        Pattern::Group(values)
1448        | Pattern::Choice(values)
1449        | Pattern::Interleave(values)
1450        | Pattern::Optional(values)
1451        | Pattern::ZeroOrMore(values)
1452        | Pattern::OneOrMore(values)
1453        | Pattern::Mixed(values) => {
1454            for child in values {
1455                collect_occurrences(child, definitions, visiting, out);
1456            }
1457        }
1458        Pattern::Grammar(_) | Pattern::ExternalRef { .. } | Pattern::ParentRef(_) => {
1459            unreachable!(
1460                "nested grammars, externalRef and parentRef are resolved by simplify_pattern"
1461            )
1462        }
1463        Pattern::Empty | Pattern::NotAllowed => {}
1464    }
1465}
1466
1467/// §7.2 (a "matches a child" and a "matches a single string" pattern must
1468/// be alternatives, both `group` and `interleave`), §7.3 (duplicate
1469/// attributes, both `group` and `interleave`) and §7.4 (overlapping
1470/// elements / duplicated text, `interleave` only).
1471fn check_pairwise(
1472    values: &[Pattern],
1473    definitions: &BTreeMap<String, Pattern>,
1474    is_interleave: bool,
1475) -> Result<(), SchemaError> {
1476    let occurrences: Vec<Occurrences> = values
1477        .iter()
1478        .map(|value| {
1479            let mut out = Occurrences::default();
1480            collect_occurrences(value, definitions, &mut BTreeSet::new(), &mut out);
1481            out
1482        })
1483        .collect();
1484    for i in 0..occurrences.len() {
1485        for j in (i + 1)..occurrences.len() {
1486            let (left, right) = (&occurrences[i], &occurrences[j]);
1487            if (left.child_capable && right.string_capable)
1488                || (left.string_capable && right.child_capable)
1489            {
1490                return Err(SchemaError::new(
1491                    "RELAX NG §7.2: a pattern matching a child and a pattern matching a single string must be alternatives",
1492                ));
1493            }
1494            for a in &occurrences[i].attributes {
1495                for b in &occurrences[j].attributes {
1496                    if name_classes_overlap(a, b) {
1497                        return Err(SchemaError::new(
1498                            "RELAX NG §7.3: a group/interleave must not allow two attributes with the same name",
1499                        ));
1500                    }
1501                }
1502            }
1503            if is_interleave {
1504                if occurrences[i].has_text && occurrences[j].has_text {
1505                    return Err(SchemaError::new(
1506                        "RELAX NG §7.4: a text pattern must not occur on both sides of an interleave",
1507                    ));
1508                }
1509                for a in &occurrences[i].elements {
1510                    for b in &occurrences[j].elements {
1511                        if name_classes_overlap(a, b) {
1512                            return Err(SchemaError::new(
1513                                "RELAX NG §7.4: an interleave must not allow overlapping element names on both sides",
1514                            ));
1515                        }
1516                    }
1517                }
1518            }
1519        }
1520    }
1521    Ok(())
1522}
1523
1524/// Walks the whole (already-flattened) schema looking for `group`/
1525/// `interleave` nodes and applying `check_pairwise` at each. Does not
1526/// descend through `ref` — every definition is walked independently by the
1527/// caller, so following refs here would just re-check shared content
1528/// (and loop forever on legal recursive grammars).
1529fn check_group_restrictions(
1530    pattern: &Pattern,
1531    definitions: &BTreeMap<String, Pattern>,
1532) -> Result<(), SchemaError> {
1533    let recurse = |values: &[Pattern]| -> Result<(), SchemaError> {
1534        values
1535            .iter()
1536            .try_for_each(|child| check_group_restrictions(child, definitions))
1537    };
1538    match pattern {
1539        Pattern::Group(values) => {
1540            check_pairwise(values, definitions, false)?;
1541            recurse(values)?;
1542        }
1543        Pattern::Interleave(values) => {
1544            check_pairwise(values, definitions, true)?;
1545            recurse(values)?;
1546        }
1547        Pattern::Element { body, .. } | Pattern::Attribute { body, .. } => recurse(body)?,
1548        Pattern::Choice(values)
1549        | Pattern::Optional(values)
1550        | Pattern::ZeroOrMore(values)
1551        | Pattern::OneOrMore(values)
1552        | Pattern::Mixed(values) => recurse(values)?,
1553        // `list` is excluded from §7.2 by spec note, and can't contain an
1554        // `attribute`, `ref` or `interleave` (§7.1.3) for §7.3/§7.4 to ever
1555        // find anything in — nothing to check inside it.
1556        // `data/except` is separately restricted to `data`/`value`/`choice`
1557        // (§7.1.4, see `check_except_restrictions`), none of which can
1558        // themselves contain a `group`/`interleave` — nothing to check.
1559        _ => {}
1560    }
1561    Ok(())
1562}
1563
1564#[cfg(test)]
1565mod tests {
1566    use super::*;
1567    use crate::{SchemaSource, SchemaSyntax};
1568
1569    struct Resolver(BTreeMap<String, SchemaSource>);
1570
1571    impl SchemaResolver for Resolver {
1572        fn resolve(
1573            &self,
1574            href: &str,
1575            _base_uri: &str,
1576        ) -> Result<SchemaSource, crate::ResolveError> {
1577            self.0
1578                .get(href)
1579                .cloned()
1580                .ok_or_else(|| crate::ResolveError::new("not found"))
1581        }
1582    }
1583
1584    fn compact(text: &str) -> SchemaDocument {
1585        crate::parse(&SchemaSource::new(
1586            text,
1587            "memory:/main.rnc",
1588            SchemaSyntax::Compact,
1589        ))
1590        .expect("schema parses")
1591    }
1592
1593    fn xml(text: &str) -> SchemaDocument {
1594        crate::parse(&SchemaSource::new(
1595            text,
1596            "memory:/main.rng",
1597            SchemaSyntax::Xml,
1598        ))
1599        .expect("schema parses")
1600    }
1601
1602    #[test]
1603    fn include_override_replaces_the_included_definition() {
1604        let resolver = Resolver(BTreeMap::from([(
1605            "base.rnc".into(),
1606            SchemaSource::new(
1607                "start = root\nroot = element base { empty }",
1608                "memory:/base.rnc",
1609                SchemaSyntax::Compact,
1610            ),
1611        )]));
1612        let schema = compact("include \"base.rnc\" { root = element override { empty } }");
1613        let compiled = simplify(&schema, &resolver).expect("schema simplifies");
1614        assert!(compiled.has_start());
1615        assert_eq!(compiled.definition_count(), 1);
1616    }
1617
1618    /// Regression test: found via `html-conform`'s Phase 05d integration —
1619    /// every real document was rejected with "unexpected element html"
1620    /// against the vendored, multi-file HTML5 schema, even though its root
1621    /// element's own `name()`/expanded-name matched exactly. Root cause:
1622    /// `include`d compact-syntax files never inherited the includer's
1623    /// `default namespace` declaration at all — `CompactParser::new`
1624    /// silently dropped the value `parse_with_inherited_ns` was already
1625    /// passing it (via a completely different field, `Context::ns`, which
1626    /// `CompactParser` never even reads). An included file with no
1627    /// `default namespace` of its own therefore always resolved its
1628    /// unprefixed element names to *no* namespace, regardless of what the
1629    /// includer declared — silently, no `SchemaError`/`ParseError`, only a
1630    /// validator that unexpectedly rejects every document. This is the
1631    /// normal, idiomatic way to structure a multi-file compact-syntax
1632    /// schema (declare `default namespace` once in the entry module, don't
1633    /// repeat it in every included one) — see `html-conform`'s vendored
1634    /// `schema/html5/*.rnc`.
1635    #[test]
1636    fn default_namespace_inherits_across_include_into_a_file_that_declares_none() {
1637        let resolver = Resolver(BTreeMap::from([(
1638            "root.rnc".into(),
1639            SchemaSource::new(
1640                "root.elem = element root { empty }",
1641                "memory:/root.rnc",
1642                SchemaSyntax::Compact,
1643            ),
1644        )]));
1645        let schema =
1646            compact("default namespace = \"urn:example\"\ninclude \"root.rnc\"\nstart = root.elem");
1647        let compiled = simplify(&schema, &resolver).expect("schema simplifies");
1648
1649        struct NamespacedRoot;
1650        impl crate::Element for NamespacedRoot {
1651            type Location = ();
1652            fn name(&self) -> crate::ExpandedName {
1653                crate::ExpandedName {
1654                    namespace: Some("urn:example".to_string()),
1655                    local: "root".to_string(),
1656                }
1657            }
1658            fn attributes(&self) -> impl Iterator<Item = (crate::ExpandedName, String)> {
1659                std::iter::empty()
1660            }
1661            fn children(&self) -> impl Iterator<Item = crate::Content<Self>> {
1662                std::iter::empty()
1663            }
1664        }
1665
1666        let registry = crate::DatatypeRegistry::new();
1667        let errors = crate::validate(&compiled, &registry, &NamespacedRoot)
1668            .expect("builtin-only registry needs no datatype support here");
1669        assert!(
1670            errors.is_empty(),
1671            "expected the namespaced root element to validate cleanly, got {errors:?}"
1672        );
1673    }
1674
1675    #[test]
1676    fn combine_requires_consistent_methods() {
1677        let resolver = Resolver(BTreeMap::new());
1678        let schema = compact("start = element root { empty }\npart |= empty\npart &= empty");
1679        assert!(simplify(&schema, &resolver).is_err());
1680    }
1681
1682    #[test]
1683    fn combine_allows_exactly_one_fragment_without_it() {
1684        let resolver = Resolver(BTreeMap::new());
1685        let schema = compact(
1686            "start = x\nx |= element foo1 { empty }\nx |= element foo2 { empty }\nx = element foo3 { empty }",
1687        );
1688        assert!(simplify(&schema, &resolver).is_ok());
1689    }
1690
1691    #[test]
1692    fn combine_rejects_more_than_one_fragment_without_it() {
1693        let resolver = Resolver(BTreeMap::new());
1694        let schema = compact(
1695            "start = x\nx = element foo1 { empty }\nx |= element foo2 { empty }\nx = element foo3 { empty }",
1696        );
1697        assert!(simplify(&schema, &resolver).is_err());
1698    }
1699
1700    #[test]
1701    fn nested_grammar_is_flattened_with_renamed_definitions() {
1702        let resolver = Resolver(BTreeMap::new());
1703        let schema = compact(
1704            "start = element root { grammar { start = foo\nfoo = element leaf { empty } } }",
1705        );
1706        let compiled = simplify(&schema, &resolver).expect("schema simplifies");
1707        assert!(compiled.has_start());
1708        assert_eq!(compiled.definition_count(), 1);
1709        let Pattern::Element { body, .. } = &compiled.start else {
1710            panic!("expected element pattern");
1711        };
1712        let Pattern::Ref(name) = &body[0] else {
1713            panic!("expected ref to renamed nested definition");
1714        };
1715        assert_ne!(name, "foo");
1716        let target = compiled
1717            .definitions
1718            .get(name)
1719            .expect("renamed definition exists");
1720        assert!(matches!(target, Pattern::Element { .. }));
1721    }
1722
1723    #[test]
1724    fn parent_ref_resolves_into_the_enclosing_scope() {
1725        let resolver = Resolver(BTreeMap::new());
1726        let schema = compact(
1727            "start = element root { grammar { start = element nested { parent shared } } }\nshared = element marker { empty }",
1728        );
1729        let compiled = simplify(&schema, &resolver).expect("schema simplifies");
1730        assert!(compiled.has_start());
1731        // The nested grammar has no `define`, only `start`, so its body is
1732        // spliced in place — only the top-level `shared` is a definition.
1733        assert_eq!(compiled.definition_count(), 1);
1734        let Pattern::Element { body, .. } = &compiled.start else {
1735            panic!("expected element pattern");
1736        };
1737        let Pattern::Element {
1738            body: nested_body, ..
1739        } = &body[0]
1740        else {
1741            panic!("expected nested element pattern");
1742        };
1743        assert_eq!(nested_body[0], Pattern::Ref("shared".into()));
1744        assert!(compiled.definitions.contains_key("shared"));
1745    }
1746
1747    #[test]
1748    fn duplicate_attribute_names_in_a_group_are_rejected() {
1749        let resolver = Resolver(BTreeMap::new());
1750        let schema =
1751            compact("start = element root { attribute id { text }, attribute id { text } }");
1752        assert!(simplify(&schema, &resolver).is_err());
1753    }
1754
1755    #[test]
1756    fn duplicate_attribute_names_reached_through_refs_are_rejected() {
1757        let resolver = Resolver(BTreeMap::new());
1758        let schema = compact(
1759            "start = element root { a, b }\na = attribute id { text }\nb = attribute id { text }",
1760        );
1761        assert!(simplify(&schema, &resolver).is_err());
1762    }
1763
1764    #[test]
1765    fn distinct_attribute_names_in_a_group_are_allowed() {
1766        let resolver = Resolver(BTreeMap::new());
1767        let schema =
1768            compact("start = element root { attribute id { text }, attribute name { text } }");
1769        assert!(simplify(&schema, &resolver).is_ok());
1770    }
1771
1772    #[test]
1773    fn wildcard_attribute_without_one_or_more_ancestor_is_rejected() {
1774        let resolver = Resolver(BTreeMap::new());
1775        let schema = compact("start = element root { attribute * { text } }");
1776        assert!(simplify(&schema, &resolver).is_err());
1777    }
1778
1779    #[test]
1780    fn attribute_cannot_contain_an_element() {
1781        let resolver = Resolver(BTreeMap::new());
1782        let schema = compact("start = element root { attribute bar { element baz { empty } } }");
1783        assert!(simplify(&schema, &resolver).is_err());
1784    }
1785
1786    #[test]
1787    fn list_cannot_contain_an_element() {
1788        let resolver = Resolver(BTreeMap::new());
1789        let schema = compact("start = element root { list { element bar { empty } } }");
1790        assert!(simplify(&schema, &resolver).is_err());
1791    }
1792
1793    #[test]
1794    fn one_or_more_cannot_wrap_a_group_containing_an_attribute() {
1795        let resolver = Resolver(BTreeMap::new());
1796        let schema =
1797            compact("start = element root { (attribute bar { text }, attribute baz { text })+ }");
1798        assert!(simplify(&schema, &resolver).is_err());
1799    }
1800
1801    #[test]
1802    fn one_or_more_around_a_bare_attribute_is_allowed() {
1803        let resolver = Resolver(BTreeMap::new());
1804        let schema = compact("start = element root { (attribute bar { text })+ }");
1805        assert!(simplify(&schema, &resolver).is_ok());
1806    }
1807
1808    #[test]
1809    fn ref_to_an_undefined_name_is_rejected() {
1810        let resolver = Resolver(BTreeMap::new());
1811        let schema = compact("start = missing");
1812        assert!(simplify(&schema, &resolver).is_err());
1813    }
1814
1815    #[test]
1816    fn ref_reaching_itself_without_an_element_is_rejected() {
1817        let resolver = Resolver(BTreeMap::new());
1818        let schema = compact("start = element root { bar }\nbar = (element bar { empty } | bar)");
1819        assert!(simplify(&schema, &resolver).is_err());
1820    }
1821
1822    #[test]
1823    fn ref_recursing_through_an_element_is_allowed() {
1824        let resolver = Resolver(BTreeMap::new());
1825        let schema = compact("start = bar\nbar = element bar { bar? }");
1826        assert!(simplify(&schema, &resolver).is_ok());
1827    }
1828
1829    #[test]
1830    fn an_unreachable_self_referencing_define_is_allowed() {
1831        let resolver = Resolver(BTreeMap::new());
1832        let schema = compact("start = element root { empty }\nunused = unused");
1833        assert!(simplify(&schema, &resolver).is_ok());
1834    }
1835
1836    #[test]
1837    fn wildcard_attribute_with_one_or_more_ancestor_is_allowed() {
1838        let resolver = Resolver(BTreeMap::new());
1839        let schema = compact("start = element root { (attribute * { text })+ }");
1840        assert!(simplify(&schema, &resolver).is_ok());
1841    }
1842
1843    #[test]
1844    fn wildcard_attribute_with_zero_or_more_ancestor_is_allowed() {
1845        // `p*` desugars to `choice(empty, oneOrMore(p))` (§4.15), so it
1846        // grants the same oneOrMore ancestor a literal `oneOrMore` would.
1847        let resolver = Resolver(BTreeMap::new());
1848        let schema = compact("start = element root { (attribute * { text })* }");
1849        assert!(simplify(&schema, &resolver).is_ok());
1850    }
1851
1852    #[test]
1853    fn wildcard_attribute_wrapped_in_one_or_more_only_at_the_ref_site_is_allowed() {
1854        // `ref` is transparent for §7.1-§7.3: a `define` may rely on every
1855        // one of its `ref` sites providing the required `oneOrMore`
1856        // ancestor, without wrapping itself — real-world schemas (e.g. the
1857        // vnu-derived HTML5 RELAX NG schema) rely on exactly this.
1858        let resolver = Resolver(BTreeMap::new());
1859        let schema = compact(
1860            "start = element root { wildcard-attr* }\nwildcard-attr = attribute * { text }",
1861        );
1862        assert!(simplify(&schema, &resolver).is_ok());
1863    }
1864
1865    #[test]
1866    fn wildcard_attribute_ref_without_any_one_or_more_wrapping_is_rejected() {
1867        let resolver = Resolver(BTreeMap::new());
1868        let schema =
1869            compact("start = element root { wildcard-attr }\nwildcard-attr = attribute * { text }");
1870        assert!(simplify(&schema, &resolver).is_err());
1871    }
1872
1873    #[test]
1874    fn duplicate_text_in_interleave_is_rejected() {
1875        let resolver = Resolver(BTreeMap::new());
1876        let schema = compact("start = element root { text & text }");
1877        assert!(simplify(&schema, &resolver).is_err());
1878    }
1879
1880    #[test]
1881    fn duplicate_referenced_element_names_in_interleave_are_rejected() {
1882        let resolver = Resolver(BTreeMap::new());
1883        let schema = compact(
1884            "start = element root { a & b }\na = element item { empty }\nb = element item { empty }",
1885        );
1886        assert!(simplify(&schema, &resolver).is_err());
1887    }
1888
1889    #[test]
1890    fn direct_duplicate_element_names_in_interleave_are_also_rejected() {
1891        // Confirmed against the official RELAX NG test suite (jing-spectest
1892        // cases for §7.4): duplicate element names in an interleave are
1893        // rejected whether reached directly or through a `ref`.
1894        let resolver = Resolver(BTreeMap::new());
1895        let schema =
1896            compact("start = element root { element item { empty } & element item { empty } }");
1897        assert!(simplify(&schema, &resolver).is_err());
1898    }
1899
1900    #[test]
1901    fn start_cannot_be_a_bare_attribute() {
1902        let resolver = Resolver(BTreeMap::new());
1903        let schema = compact("start = attribute foo { text }");
1904        assert!(simplify(&schema, &resolver).is_err());
1905    }
1906
1907    #[test]
1908    fn a_bare_pattern_root_is_also_checked_against_start_restrictions() {
1909        let resolver = Resolver(BTreeMap::new());
1910        let schema = compact("text");
1911        assert!(simplify(&schema, &resolver).is_err());
1912    }
1913
1914    #[test]
1915    fn data_except_cannot_contain_text() {
1916        let resolver = Resolver(BTreeMap::new());
1917        let schema = compact("start = element root { string {} - text }");
1918        assert!(simplify(&schema, &resolver).is_err());
1919    }
1920
1921    #[test]
1922    fn data_except_with_a_value_is_allowed() {
1923        let resolver = Resolver(BTreeMap::new());
1924        let schema = compact("start = element root { string {} - \"foo\" }");
1925        assert!(simplify(&schema, &resolver).is_ok());
1926    }
1927
1928    #[test]
1929    fn unknown_type_in_the_built_in_datatype_library_is_rejected() {
1930        // No `datatypeLibrary` anywhere — §4.3 says that defaults to the
1931        // empty string (the built-in library), which only has `string`
1932        // and `token`.
1933        let resolver = Resolver(BTreeMap::new());
1934        let schema = xml(
1935            r#"<element xmlns="http://relaxng.org/ns/structure/1.0" name="foo"><data type="tok"/></element>"#,
1936        );
1937        assert!(simplify(&schema, &resolver).is_err());
1938    }
1939
1940    #[test]
1941    fn datatype_library_is_inherited_from_an_ancestor_element() {
1942        let resolver = Resolver(BTreeMap::new());
1943        let schema = xml(
1944            r#"<element xmlns="http://relaxng.org/ns/structure/1.0" name="foo" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes"><data type="NCName"/></element>"#,
1945        );
1946        assert!(simplify(&schema, &resolver).is_ok());
1947    }
1948
1949    #[test]
1950    fn a_string_pattern_sequenced_with_an_element_is_rejected() {
1951        let resolver = Resolver(BTreeMap::new());
1952        let schema = compact("start = element root { string, element child { empty } }");
1953        assert!(simplify(&schema, &resolver).is_err());
1954    }
1955
1956    #[test]
1957    fn a_string_pattern_as_an_alternative_to_an_element_is_allowed() {
1958        let resolver = Resolver(BTreeMap::new());
1959        let schema = compact("start = element root { string | element child { empty } }");
1960        assert!(simplify(&schema, &resolver).is_ok());
1961    }
1962
1963    #[test]
1964    fn string_sequences_inside_list_are_not_restricted_by_7_2() {
1965        let resolver = Resolver(BTreeMap::new());
1966        let schema = compact("start = element root { list { string, string } }");
1967        assert!(simplify(&schema, &resolver).is_ok());
1968    }
1969
1970    #[test]
1971    fn external_reference_is_resolved_through_the_caller() {
1972        let resolver = Resolver(BTreeMap::from([(
1973            "part.rnc".into(),
1974            SchemaSource::new(
1975                "element child { empty }",
1976                "memory:/part.rnc",
1977                SchemaSyntax::Compact,
1978            ),
1979        )]));
1980        let schema = compact("start = external \"part.rnc\"");
1981        assert!(
1982            simplify(&schema, &resolver)
1983                .expect("external pattern simplifies")
1984                .has_start()
1985        );
1986    }
1987}