Skip to main content

xsd_schema/compiler/
compile.rs

1//! NFA compilation functions
2//!
3//! This module implements the core compilation logic for transforming
4//! XSD content model particles into NFAs.
5
6use crate::arenas::{ComplexTypeDefData, ModelGroupData};
7use crate::ids::ModelGroupKey;
8use crate::ids::{ElementKey, NameId, TypeKey};
9use crate::parser::frames::DerivationMethod;
10use crate::parser::frames::{
11    ComplexContentResult, Compositor, ElementFrameResult, ModelGroupDefResult, NamespaceToken,
12    NotQNameItem, OpenContentResult, ParticleResult, ParticleTerm, ProcessContents, QNameRef,
13    TypeRefResult, WildcardNamespace, WildcardResult,
14};
15use crate::parser::location::SourceRef;
16use crate::schema::model::{DefaultOpenContent, XsdVersion};
17use crate::schema::wildcard::{ElementWildcard, NamespaceConstraint as SchemaNamespaceConstraint};
18#[cfg(test)]
19use crate::schema::FormChoice;
20use crate::schema::SchemaSet;
21use crate::types::complex::{
22    NamespaceConstraint, OpenContent, OpenContentMode as TypesOpenContentMode,
23    ProcessContents as TypesProcessContents, WildcardRef,
24};
25
26use super::all_group::{
27    AllGroupModel, AllParticle, OpenContentMode as AllGroupOpenContentMode, OpenContentWildcard,
28};
29use super::error::{NfaCompileError, NfaCompileResult};
30use super::fragment::{fragment_to_table, FragmentBuilder, NfaFragment};
31use super::nfa::{NfaTable, NfaTerm};
32use super::particle::{apply_occurs, MaxOccurs};
33use super::ContentModelMatcher;
34
35/// Maximum recursion depth for compiling nested groups
36const MAX_RECURSION_DEPTH: usize = 100;
37
38/// Context for NFA compilation
39///
40/// Provides access to the schema set for resolving references during compilation.
41pub struct CompileContext<'a> {
42    /// Reference to the schema set for resolving references
43    pub schema_set: &'a SchemaSet,
44    /// Target namespace for the content model being compiled
45    pub target_namespace: Option<NameId>,
46    /// Fragment builder for constructing NFA fragments
47    builder: FragmentBuilder,
48    /// Current recursion depth
49    depth: usize,
50    /// Resolved types from resolved_particles (set when compiling a model group)
51    resolved_particle_types: Vec<Option<TypeKey>>,
52    /// Current particle index within the model group being compiled
53    current_particle_idx: Option<usize>,
54    /// Flat depth-first element counter for content particle compilation.
55    /// When Some, overrides per-level `current_particle_idx` for type resolution.
56    content_flat_idx: Option<usize>,
57    /// Resolved element keys for local elements in content particles (flat depth-first order)
58    resolved_particle_elements: Vec<Option<ElementKey>>,
59    /// Current sibling element QNames for ##definedSibling expansion in wildcard compilation.
60    /// Set before compiling particles in a model group (sequence/choice/all).
61    current_sibling_elements: Vec<(Option<NameId>, NameId)>,
62    /// When compiling a redefining group, stores (group_name, group_ns, original_key)
63    /// so self-referencing QName refs are redirected to the original group.
64    redefine_redirect: Option<(NameId, Option<NameId>, ModelGroupKey)>,
65    /// When true, occurrence bounds are capped for UPA checking.
66    /// Produces counter-free NFAs suitable for epsilon-closure-based UPA analysis.
67    upa_mode: bool,
68}
69
70impl<'a> CompileContext<'a> {
71    /// Create a new compilation context
72    pub fn new(schema_set: &'a SchemaSet, target_namespace: Option<NameId>) -> Self {
73        Self {
74            schema_set,
75            target_namespace,
76            builder: FragmentBuilder::new(),
77            depth: 0,
78            resolved_particle_types: Vec::new(),
79            current_particle_idx: None,
80            content_flat_idx: None,
81            resolved_particle_elements: Vec::new(),
82            current_sibling_elements: Vec::new(),
83            redefine_redirect: None,
84            upa_mode: false,
85        }
86    }
87
88    /// Create a compilation context with UPA occurrence-bound capping enabled.
89    pub fn new_for_upa(schema_set: &'a SchemaSet, target_namespace: Option<NameId>) -> Self {
90        Self {
91            upa_mode: true,
92            ..Self::new(schema_set, target_namespace)
93        }
94    }
95
96    /// Compile a particle to an NFA table
97    ///
98    /// This is the main entry point for compiling a content model particle.
99    pub fn compile_particle(&mut self, particle: &ParticleResult) -> NfaCompileResult<NfaTable> {
100        self.check_recursion(particle.source.as_ref())?;
101        self.depth += 1;
102
103        let fragment = self.compile_particle_to_fragment(particle)?;
104        let table = fragment_to_table(fragment);
105
106        self.depth -= 1;
107        Ok(table)
108    }
109
110    /// Compile a model group to an NFA table
111    ///
112    /// Used for compiling named groups (xs:group).
113    pub fn compile_model_group(
114        &mut self,
115        group: &ModelGroupDefResult,
116    ) -> NfaCompileResult<NfaTable> {
117        self.check_recursion(group.source.as_ref())?;
118        self.depth += 1;
119
120        let fragment = self.compile_model_group_to_fragment(group)?;
121        let table = fragment_to_table(fragment);
122
123        self.depth -= 1;
124        Ok(table)
125    }
126
127    /// Compile a particle to a fragment (internal use)
128    fn compile_particle_to_fragment(
129        &mut self,
130        particle: &ParticleResult,
131    ) -> NfaCompileResult<NfaFragment> {
132        // Validate occurrence constraints
133        if let Some(max) = particle.max_occurs {
134            if particle.min_occurs > max {
135                return Err(NfaCompileError::invalid_occurrence(
136                    particle.min_occurs,
137                    max,
138                    particle.source.clone(),
139                ));
140            }
141        }
142
143        // Compile the term
144        let term_fragment = self.compile_term(&particle.term, particle.source.as_ref())?;
145
146        // Apply occurrence constraints
147        let fragment =
148            self.apply_occurrences(term_fragment, particle.min_occurs, particle.max_occurs);
149
150        Ok(fragment)
151    }
152
153    /// Compile a particle term to a fragment
154    fn compile_term(
155        &mut self,
156        term: &ParticleTerm,
157        source: Option<&SourceRef>,
158    ) -> NfaCompileResult<NfaFragment> {
159        match term {
160            ParticleTerm::Element(elem) => self.compile_element(elem, source),
161            ParticleTerm::Group(group) => self.compile_model_group_to_fragment(group),
162            ParticleTerm::Any(wildcard) => self.compile_wildcard(wildcard, source),
163        }
164    }
165
166    /// Build an NfaTerm for an element declaration, resolving name, namespace,
167    /// element_key, and type information.
168    ///
169    /// This is the shared logic used by both `compile_element()` (NFA path)
170    /// and `compile_all_group_model()` (AllGroup path).
171    fn build_element_term(
172        &mut self,
173        elem: &ElementFrameResult,
174        source: Option<&SourceRef>,
175    ) -> NfaCompileResult<NfaTerm> {
176        // Grab and increment flat element index (if compiling content particles)
177        let current_flat_idx = if let Some(flat_idx) = self.content_flat_idx {
178            self.content_flat_idx = Some(flat_idx + 1);
179            Some(flat_idx)
180        } else {
181            None
182        };
183
184        // Determine element name and namespace
185        let (name, namespace, element_key) = if let Some(ref_name) = &elem.ref_name {
186            // An unresolved ref with an explicit namespace is an error
187            // (§4.2.4: imports are not transitive). No-namespace refs
188            // stay lenient — chameleon-adopted target components leave
189            // such refs pointing at empty-namespace names that no
190            // longer exist, but the owning complex type is replaced by
191            // an override before any instance reaches it.
192            let key = self
193                .schema_set
194                .lookup_element(ref_name.namespace, ref_name.local_name);
195            if key.is_none() && ref_name.namespace.is_some() {
196                let name_str = crate::schema::resolver::format_resolved_qname(
197                    &self.schema_set.name_table,
198                    ref_name.namespace,
199                    ref_name.local_name,
200                );
201                return Err(NfaCompileError::unresolved_element(
202                    name_str,
203                    elem.source.clone().or_else(|| source.cloned()),
204                ));
205            }
206            (ref_name.local_name, ref_name.namespace, key)
207        } else if let Some(name) = elem.name {
208            // Local element declaration
209            let source_ref = source.or(elem.source.as_ref());
210            let namespace = self.effective_element_namespace(elem, source_ref);
211            // Look up local element key from resolved_particle_elements
212            let local_key = current_flat_idx
213                .and_then(|idx| self.resolved_particle_elements.get(idx).copied().flatten())
214                .or_else(|| {
215                    self.current_particle_idx
216                        .and_then(|idx| self.resolved_particle_elements.get(idx).copied().flatten())
217                });
218            (name, namespace, local_key)
219        } else {
220            return Err(NfaCompileError::unresolved_element(
221                "anonymous element without name or ref".to_string(),
222                source.cloned(),
223            ));
224        };
225
226        // For local elements (element_key is None), resolve type
227        let resolved_type = if element_key.is_none() {
228            // First: check context for resolved type
229            let type_from_context = if let Some(flat_idx) = current_flat_idx {
230                self.resolved_particle_types
231                    .get(flat_idx)
232                    .copied()
233                    .flatten()
234            } else {
235                self.current_particle_idx
236                    .and_then(|idx| self.resolved_particle_types.get(idx).copied().flatten())
237            };
238            // Then: try QName resolution
239            type_from_context.or_else(|| self.resolve_element_type_ref(elem))
240        } else {
241            None // Elements with key get type from element declaration via arena
242        };
243
244        Ok(NfaTerm::element_with_type(
245            name,
246            namespace,
247            element_key,
248            resolved_type,
249        ))
250    }
251
252    /// Compile an element to a fragment
253    fn compile_element(
254        &mut self,
255        elem: &ElementFrameResult,
256        source: Option<&SourceRef>,
257    ) -> NfaCompileResult<NfaFragment> {
258        let nfa_term = self.build_element_term(elem, source)?;
259        Ok(self.builder.single_term(nfa_term, source.cloned()))
260    }
261
262    /// Resolve a local element's QName type reference to a TypeKey
263    fn resolve_element_type_ref(&self, elem: &ElementFrameResult) -> Option<TypeKey> {
264        match &elem.type_ref {
265            Some(TypeRefResult::QName(qname)) => self
266                .schema_set
267                .lookup_type(qname.namespace, qname.local_name)
268                .or_else(|| {
269                    self.schema_set
270                        .get_built_in_type_by_qname(qname.namespace, qname.local_name)
271                }),
272            _ => None, // Inline types not resolved at compile time
273        }
274    }
275
276    /// Compile a wildcard to a fragment
277    fn compile_wildcard(
278        &mut self,
279        wildcard: &WildcardResult,
280        source: Option<&SourceRef>,
281    ) -> NfaCompileResult<NfaFragment> {
282        let mut namespace_constraint = self.convert_wildcard_namespace(&wildcard.namespace);
283        let process_contents = self.convert_process_contents(wildcard.process_contents);
284
285        // Override with notNamespace if present
286        if let Some(not_ns) = self.convert_not_namespace(&wildcard.not_namespace) {
287            namespace_constraint = not_ns;
288        }
289
290        // Expand notQName items — use current_sibling_elements for ##definedSibling
291        let not_qnames = self.expand_not_qnames(&wildcard.not_qname);
292
293        let nfa_term =
294            NfaTerm::wildcard_with_not_qnames(namespace_constraint, process_contents, not_qnames);
295        Ok(self.builder.single_term(nfa_term, source.cloned()))
296    }
297
298    /// Expand NotQNameItems into concrete (namespace, local_name) pairs.
299    /// Uses `self.current_sibling_elements` for ##definedSibling expansion.
300    fn expand_not_qnames(&self, items: &[NotQNameItem]) -> Vec<(Option<NameId>, NameId)> {
301        let mut result = Vec::new();
302        for item in items {
303            match item {
304                NotQNameItem::QName {
305                    namespace,
306                    local_name,
307                } => {
308                    result.push((*namespace, *local_name));
309                }
310                NotQNameItem::Defined => {
311                    result.extend(expand_defined_element_qnames(self.schema_set));
312                }
313                NotQNameItem::DefinedSibling => {
314                    result.extend_from_slice(&self.current_sibling_elements);
315                }
316            }
317        }
318        result
319    }
320
321    /// Compile a model group definition to a fragment
322    fn compile_model_group_to_fragment(
323        &mut self,
324        group: &ModelGroupDefResult,
325    ) -> NfaCompileResult<NfaFragment> {
326        // If this is a group reference, resolve and compile the referenced group
327        if let Some(ref_name) = &group.ref_name {
328            return self.compile_group_ref(ref_name, group.source.as_ref());
329        }
330
331        // Get the compositor, default to sequence
332        let compositor = group.compositor.unwrap_or(Compositor::Sequence);
333
334        // Handle empty particle list
335        if group.particles.is_empty() {
336            return Ok(self.builder.epsilon_fragment());
337        }
338
339        // Compile based on compositor type
340        match compositor {
341            Compositor::Sequence => self.compile_sequence(&group.particles),
342            Compositor::Choice => self.compile_choice(&group.particles),
343            Compositor::All => self.compile_all(&group.particles, group.source.as_ref()),
344        }
345    }
346
347    /// Compile a particle with a tracked index for resolved type lookup
348    fn compile_particle_with_index(
349        &mut self,
350        particle: &ParticleResult,
351        particle_idx: usize,
352    ) -> NfaCompileResult<NfaFragment> {
353        if self.content_flat_idx.is_some() {
354            // When using flat element counter, skip per-level positional indexing
355            return self.compile_particle_to_fragment(particle);
356        }
357        let saved_idx = self.current_particle_idx;
358        self.current_particle_idx = Some(particle_idx);
359        let result = self.compile_particle_to_fragment(particle);
360        self.current_particle_idx = saved_idx;
361        result
362    }
363
364    /// Compile a sequence (xs:sequence)
365    fn compile_sequence(&mut self, particles: &[ParticleResult]) -> NfaCompileResult<NfaFragment> {
366        if particles.is_empty() {
367            return Ok(self.builder.epsilon_fragment());
368        }
369
370        // Set sibling elements for ##definedSibling expansion in wildcards
371        let new_siblings = self.collect_sibling_element_qnames(particles);
372        let saved_siblings = std::mem::replace(&mut self.current_sibling_elements, new_siblings);
373
374        let mut result = self.compile_particle_with_index(&particles[0], 0)?;
375        for (i, particle) in particles[1..].iter().enumerate() {
376            let frag = self.compile_particle_with_index(particle, i + 1)?;
377            result = result.concat(frag);
378        }
379
380        self.current_sibling_elements = saved_siblings;
381        Ok(result)
382    }
383
384    /// Compile a choice (xs:choice)
385    fn compile_choice(&mut self, particles: &[ParticleResult]) -> NfaCompileResult<NfaFragment> {
386        if particles.is_empty() {
387            return Ok(self.builder.epsilon_fragment());
388        }
389
390        // Set sibling elements for ##definedSibling expansion in wildcards
391        let new_siblings = self.collect_sibling_element_qnames(particles);
392        let saved_siblings = std::mem::replace(&mut self.current_sibling_elements, new_siblings);
393
394        let mut result = self.compile_particle_with_index(&particles[0], 0)?;
395        for (i, particle) in particles[1..].iter().enumerate() {
396            let frag = self.compile_particle_with_index(particle, i + 1)?;
397            result = result.alternate(frag);
398        }
399
400        self.current_sibling_elements = saved_siblings;
401        Ok(result)
402    }
403
404    /// Compile an all-group (xs:all) as NFA — used as fallback for nested
405    /// all-groups that cannot use `AllGroupModel` (e.g. inside a sequence
406    /// or choice). Top-level all-groups (both inline and named refs) use
407    /// `compile_all_group_model()` instead.
408    fn compile_all(
409        &mut self,
410        particles: &[ParticleResult],
411        source: Option<&SourceRef>,
412    ) -> NfaCompileResult<NfaFragment> {
413        // Validate XSD 1.0 constraints
414        for particle in particles {
415            if !matches!(particle.term, ParticleTerm::Element(_)) {
416                return Err(NfaCompileError::invalid_all_group(source.cloned()));
417            }
418
419            if let Some(max) = particle.max_occurs {
420                if max > 1 {
421                    return Err(NfaCompileError::invalid_all_group(source.cloned()));
422                }
423            } else {
424                return Err(NfaCompileError::invalid_all_group(source.cloned()));
425            }
426        }
427
428        if particles.is_empty() {
429            return Ok(self.builder.epsilon_fragment());
430        }
431
432        // Choice-of-all with bounded repeat: allows any order but doesn't
433        // enforce "each element at most once". Named-group all-groups land
434        // here; inline top-level all-groups use AllGroupModel instead.
435        let mut choice = self.compile_particle_with_index(&particles[0], 0)?;
436        for (i, particle) in particles[1..].iter().enumerate() {
437            let frag = self.compile_particle_with_index(particle, i + 1)?;
438            choice = choice.alternate(frag);
439        }
440
441        let n = particles.len() as u32;
442        Ok(choice.repeat_range(0, Some(n)))
443    }
444
445    /// Compile an all-group's particles into an `AllGroupModel`.
446    ///
447    /// Each particle is resolved to an `AllParticle` with its `NfaTerm`,
448    /// min/max occurs, and source location. Element and wildcard particles
449    /// are supported directly. In XSD 1.1, group references inside all-groups
450    /// are flattened per cos-all-limited constraints (the referenced group must
451    /// be an all-group with minOccurs=maxOccurs=1). XSD 1.0 rejects group refs.
452    fn compile_all_group_model(
453        &mut self,
454        particles: &[ParticleResult],
455        source: Option<&SourceRef>,
456    ) -> NfaCompileResult<AllGroupModel> {
457        // Set sibling elements for ##definedSibling expansion in wildcards
458        let new_siblings = self.collect_sibling_element_qnames(particles);
459        let saved_siblings = std::mem::replace(&mut self.current_sibling_elements, new_siblings);
460
461        let mut all_particles = Vec::with_capacity(particles.len());
462
463        for particle in particles {
464            let term = match &particle.term {
465                ParticleTerm::Element(elem) => {
466                    self.build_element_term(elem, particle.source.as_ref().or(source))?
467                }
468                ParticleTerm::Any(wildcard) => {
469                    let mut ns = self.convert_wildcard_namespace(&wildcard.namespace);
470                    let pc = self.convert_process_contents(wildcard.process_contents);
471                    // Override with notNamespace if present
472                    if let Some(not_ns) = self.convert_not_namespace(&wildcard.not_namespace) {
473                        ns = not_ns;
474                    }
475                    let not_qnames = self.expand_not_qnames(&wildcard.not_qname);
476                    NfaTerm::wildcard_with_not_qnames(ns, pc, not_qnames)
477                }
478                #[cfg(feature = "xsd11")]
479                ParticleTerm::Group(group) => {
480                    // XSD 1.0 forbids group refs inside xs:all even in an xsd11 build
481                    if !self.schema_set.is_xsd11() {
482                        return Err(NfaCompileError::invalid_all_group(
483                            particle.source.clone().or_else(|| source.cloned()),
484                        ));
485                    }
486                    // cos-all-limited 1.3: minOccurs = maxOccurs = 1
487                    if particle.min_occurs != 1 || particle.max_occurs != Some(1) {
488                        return Err(NfaCompileError::InvalidAllGroupOccurs {
489                            reason: "cos-all-limited.1.3: group reference inside xs:all \
490                                     must have minOccurs = maxOccurs = 1"
491                                .into(),
492                            location: particle.source.clone().or_else(|| source.cloned()),
493                        });
494                    }
495                    // Must be a group reference, not an inline group.
496                    if group.ref_name.is_none() {
497                        return Err(NfaCompileError::invalid_all_group(
498                            particle.source.clone().or_else(|| source.cloned()),
499                        ));
500                    }
501                    // Flatten: resolve group ref, verify compositor, inline particles
502                    self.flatten_all_group_ref_into(
503                        group,
504                        particle.source.as_ref().or(source),
505                        &mut all_particles,
506                    )?;
507                    continue; // particles already added, skip the push below
508                }
509                #[cfg(not(feature = "xsd11"))]
510                ParticleTerm::Group(_) => {
511                    return Err(NfaCompileError::invalid_all_group(source.cloned()));
512                }
513            };
514
515            let max_occurs = MaxOccurs::from_option(particle.max_occurs);
516            all_particles.push(AllParticle::new(
517                term,
518                particle.min_occurs,
519                max_occurs,
520                particle.source.clone().or_else(|| source.cloned()),
521            ));
522        }
523
524        self.current_sibling_elements = saved_siblings;
525        Ok(AllGroupModel::new(all_particles))
526    }
527
528    /// Flatten a group reference inside an xs:all group into the parent's
529    /// `all_particles` vector.
530    ///
531    /// **Preconditions** (checked by caller):
532    /// - `group.ref_name` is `Some` (this is a group reference, not inline)
533    /// - The containing particle has `minOccurs = maxOccurs = 1`
534    ///
535    /// This method resolves the group ref, verifies the referenced group's
536    /// compositor is `All` (cos-all-limited rule 2), then recursively compiles
537    /// each inner particle into the parent's `all_particles`.
538    #[cfg(feature = "xsd11")]
539    fn flatten_all_group_ref_into(
540        &mut self,
541        group: &ModelGroupDefResult,
542        source: Option<&SourceRef>,
543        all_particles: &mut Vec<AllParticle>,
544    ) -> NfaCompileResult<()> {
545        // Recursion guard
546        self.check_recursion(source)?;
547        self.depth += 1;
548
549        let ref_name = group
550            .ref_name
551            .as_ref()
552            .expect("caller checked ref_name is Some");
553
554        // Resolve the group ref (redefine-aware)
555        let group_key = self.resolve_model_group_key(ref_name).ok_or_else(|| {
556            let name = format!(
557                "{}:{}",
558                ref_name
559                    .namespace
560                    .map(|n| format!("{:?}", n))
561                    .unwrap_or_default(),
562                ref_name.local_name.0
563            );
564            NfaCompileError::unresolved_group(name, source.cloned())
565        })?;
566
567        let group_data = self
568            .schema_set
569            .arenas
570            .get_model_group(group_key)
571            .ok_or_else(|| {
572                NfaCompileError::unresolved_group(
573                    format!("group key {:?}", group_key),
574                    source.cloned(),
575                )
576            })?;
577
578        // cos-all-limited rule 2: compositor must be All
579        let compositor = group_data.compositor.unwrap_or(Compositor::Sequence);
580        if compositor != Compositor::All {
581            self.depth -= 1;
582            return Err(NfaCompileError::InvalidAllGroupContent {
583                location: source.cloned(),
584            });
585        }
586
587        // Save context and set up flat indexing for the referenced group
588        let saved_flat_idx = self.content_flat_idx.take();
589        let saved_particle_elements = std::mem::take(&mut self.resolved_particle_elements);
590        let saved_types = std::mem::take(&mut self.resolved_particle_types);
591        let saved_idx = self.current_particle_idx;
592
593        self.resolved_particle_types = group_data.resolved_particle_types.clone();
594        self.resolved_particle_elements = group_data.resolved_particle_elements.clone();
595        self.content_flat_idx = Some(0);
596
597        // Compile each inner particle
598        let result = self.flatten_all_group_particles(&group_data.particles, source, all_particles);
599
600        // Restore context
601        self.content_flat_idx = saved_flat_idx;
602        self.resolved_particle_elements = saved_particle_elements;
603        self.resolved_particle_types = saved_types;
604        self.current_particle_idx = saved_idx;
605        self.depth -= 1;
606
607        result
608    }
609
610    /// Compile inner particles of a referenced all-group into the parent's
611    /// `all_particles` vector. Called by `flatten_all_group_ref_into`.
612    #[cfg(feature = "xsd11")]
613    fn flatten_all_group_particles(
614        &mut self,
615        particles: &[ParticleResult],
616        source: Option<&SourceRef>,
617        all_particles: &mut Vec<AllParticle>,
618    ) -> NfaCompileResult<()> {
619        for particle in particles {
620            let term = match &particle.term {
621                ParticleTerm::Element(elem) => {
622                    self.build_element_term(elem, particle.source.as_ref().or(source))?
623                }
624                ParticleTerm::Any(wildcard) => {
625                    let mut ns = self.convert_wildcard_namespace(&wildcard.namespace);
626                    let pc = self.convert_process_contents(wildcard.process_contents);
627                    if let Some(not_ns) = self.convert_not_namespace(&wildcard.not_namespace) {
628                        ns = not_ns;
629                    }
630                    let not_qnames = self.expand_not_qnames(&wildcard.not_qname);
631                    NfaTerm::wildcard_with_not_qnames(ns, pc, not_qnames)
632                }
633                ParticleTerm::Group(inner_group) => {
634                    // Nested group ref inside the referenced all-group — recurse
635                    // cos-all-limited 1.3: minOccurs = maxOccurs = 1
636                    if particle.min_occurs != 1 || particle.max_occurs != Some(1) {
637                        return Err(NfaCompileError::InvalidAllGroupOccurs {
638                            reason: "cos-all-limited.1.3: group reference inside xs:all \
639                                     must have minOccurs = maxOccurs = 1"
640                                .into(),
641                            location: particle.source.clone().or_else(|| source.cloned()),
642                        });
643                    }
644                    if inner_group.ref_name.is_none() {
645                        return Err(NfaCompileError::invalid_all_group(
646                            particle.source.clone().or_else(|| source.cloned()),
647                        ));
648                    }
649                    self.flatten_all_group_ref_into(
650                        inner_group,
651                        particle.source.as_ref().or(source),
652                        all_particles,
653                    )?;
654                    continue;
655                }
656            };
657
658            let max_occurs = MaxOccurs::from_option(particle.max_occurs);
659            all_particles.push(AllParticle::new(
660                term,
661                particle.min_occurs,
662                max_occurs,
663                particle.source.clone().or_else(|| source.cloned()),
664            ));
665        }
666        Ok(())
667    }
668
669    /// Compile a group reference
670    fn compile_group_ref(
671        &mut self,
672        ref_name: &QNameRef,
673        source: Option<&SourceRef>,
674    ) -> NfaCompileResult<NfaFragment> {
675        // Check recursion depth to detect circular group references
676        self.check_recursion(source)?;
677        self.depth += 1;
678
679        // Look up the referenced group (redefine-aware)
680        let group_key = self.resolve_model_group_key(ref_name).ok_or_else(|| {
681            let name = format!(
682                "{}:{}",
683                ref_name
684                    .namespace
685                    .map(|n| format!("{:?}", n))
686                    .unwrap_or_default(),
687                ref_name.local_name.0
688            );
689            NfaCompileError::unresolved_group(name, source.cloned())
690        })?;
691
692        // Get the group data from arenas
693        let group_data = self
694            .schema_set
695            .arenas
696            .get_model_group(group_key)
697            .ok_or_else(|| {
698                NfaCompileError::unresolved_group(
699                    format!("group key {:?}", group_key),
700                    source.cloned(),
701                )
702            })?;
703
704        // Convert ModelGroupData particles to fragments
705        let result = self.compile_model_group_data(group_data, source);
706        self.depth -= 1;
707        result
708    }
709
710    /// Resolve a model group reference QName to a key, redirecting self-references
711    /// in redefining groups to the original group to avoid infinite recursion.
712    fn resolve_model_group_key(&self, ref_name: &QNameRef) -> Option<ModelGroupKey> {
713        if let Some((name, ns, original_key)) = self.redefine_redirect {
714            if ref_name.local_name == name && ref_name.namespace == ns {
715                return Some(original_key);
716            }
717        }
718        self.schema_set
719            .lookup_model_group(ref_name.namespace, ref_name.local_name)
720    }
721
722    /// Compile from ModelGroupData (arena storage format)
723    fn compile_model_group_data(
724        &mut self,
725        group: &ModelGroupData,
726        source: Option<&SourceRef>,
727    ) -> NfaCompileResult<NfaFragment> {
728        let compositor = group.compositor.unwrap_or(Compositor::Sequence);
729
730        if group.particles.is_empty() {
731            return Ok(self.builder.epsilon_fragment());
732        }
733
734        // Save context and set up flat indexing for the named group
735        let saved_flat_idx = self.content_flat_idx.take();
736        let saved_particle_elements = std::mem::take(&mut self.resolved_particle_elements);
737        let saved_types = std::mem::take(&mut self.resolved_particle_types);
738        let saved_idx = self.current_particle_idx;
739
740        // Set up redefine redirect if this is a redefining group
741        let saved_redirect = self.redefine_redirect.take();
742        if let (Some(original_key), Some(name)) = (group.redefine_original, group.name) {
743            self.redefine_redirect = Some((name, group.target_namespace, original_key));
744        }
745
746        // Use flat-indexed fields from ModelGroupData
747        self.resolved_particle_types = group.resolved_particle_types.clone();
748        self.resolved_particle_elements = group.resolved_particle_elements.clone();
749        // Enable flat indexing within the group
750        self.content_flat_idx = Some(0);
751
752        let result = match compositor {
753            Compositor::Sequence => self.compile_sequence(&group.particles),
754            Compositor::Choice => self.compile_choice(&group.particles),
755            Compositor::All => self.compile_all(&group.particles, source),
756        };
757
758        // Restore previous context
759        self.redefine_redirect = saved_redirect;
760        self.content_flat_idx = saved_flat_idx;
761        self.resolved_particle_elements = saved_particle_elements;
762        self.resolved_particle_types = saved_types;
763        self.current_particle_idx = saved_idx;
764        result
765    }
766
767    /// Apply occurrence constraints to a fragment
768    ///
769    /// Small maxOccurs are unrolled; large values use counted NFA transitions;
770    /// very large values (> MAX_COUNTED_OCCURS) fall back to unbounded.
771    ///
772    /// When `upa_mode` is true, bounds are capped to small values first,
773    /// producing a counter-free NFA suitable for UPA analysis.
774    fn apply_occurrences(
775        &mut self,
776        fragment: NfaFragment,
777        min: u32,
778        max: Option<u32>,
779    ) -> NfaFragment {
780        let (eff_min, eff_max) = if self.upa_mode {
781            cap_for_upa(min, max)
782        } else {
783            (min, max)
784        };
785        let max_occurs = MaxOccurs::from_option(eff_max);
786        apply_occurs(fragment, eff_min, max_occurs)
787    }
788
789    /// Check recursion depth
790    fn check_recursion(&self, source: Option<&SourceRef>) -> NfaCompileResult<()> {
791        if self.depth >= MAX_RECURSION_DEPTH {
792            return Err(NfaCompileError::recursion_exceeded(source.cloned()));
793        }
794        Ok(())
795    }
796
797    /// Convert WildcardNamespace to NamespaceConstraint
798    fn convert_wildcard_namespace(&self, ns: &WildcardNamespace) -> NamespaceConstraint {
799        match ns {
800            WildcardNamespace::Any => NamespaceConstraint::Any,
801            WildcardNamespace::Other => NamespaceConstraint::Other,
802            WildcardNamespace::TargetNamespace => NamespaceConstraint::TargetNamespace,
803            WildcardNamespace::Local => NamespaceConstraint::Local,
804            WildcardNamespace::List(list) => NamespaceConstraint::List(
805                list.iter()
806                    .map(|t| t.resolve(self.target_namespace))
807                    .collect(),
808            ),
809        }
810    }
811
812    /// Convert WildcardResult's not_namespace to NamespaceConstraint::Not if non-empty.
813    /// Returns None if not_namespace is empty (no override).
814    fn convert_not_namespace(
815        &self,
816        not_namespace: &[NamespaceToken],
817    ) -> Option<NamespaceConstraint> {
818        if not_namespace.is_empty() {
819            return None;
820        }
821        let excluded: Vec<Option<NameId>> = not_namespace
822            .iter()
823            .map(|t| t.resolve(self.target_namespace))
824            .collect();
825        Some(NamespaceConstraint::Not(excluded))
826    }
827
828    /// Convert parser ProcessContents to types ProcessContents
829    fn convert_process_contents(&self, pc: ProcessContents) -> TypesProcessContents {
830        match pc {
831            ProcessContents::Strict => TypesProcessContents::Strict,
832            ProcessContents::Lax => TypesProcessContents::Lax,
833            ProcessContents::Skip => TypesProcessContents::Skip,
834        }
835    }
836
837    fn effective_element_namespace(
838        &self,
839        elem: &ElementFrameResult,
840        source: Option<&SourceRef>,
841    ) -> Option<NameId> {
842        self.schema_set.effective_local_element_namespace(
843            elem.target_namespace,
844            elem.form.as_deref(),
845            source,
846            self.target_namespace,
847        )
848    }
849
850    /// Collect sibling element QNames from a particle list, using proper
851    /// namespace resolution (element refs, form attribute, elementFormDefault).
852    /// Used for ##definedSibling expansion. Recurses into group refs to
853    /// include elements from referenced groups.
854    fn collect_sibling_element_qnames(
855        &self,
856        particles: &[ParticleResult],
857    ) -> Vec<(Option<NameId>, NameId)> {
858        self.collect_sibling_element_qnames_inner(particles, 0)
859    }
860
861    fn collect_sibling_element_qnames_inner(
862        &self,
863        particles: &[ParticleResult],
864        depth: usize,
865    ) -> Vec<(Option<NameId>, NameId)> {
866        // Defense-in-depth: bail on unreasonably deep nesting
867        if depth >= MAX_RECURSION_DEPTH {
868            return Vec::new();
869        }
870        let mut result = Vec::new();
871        for p in particles {
872            match &p.term {
873                ParticleTerm::Element(elem) => {
874                    if let Some(ref_name) = &elem.ref_name {
875                        // Element reference — use the ref's resolved QName
876                        result.push((ref_name.namespace, ref_name.local_name));
877                        // §3.10.6.1 ##definedSibling also excludes substitution-
878                        // group members of any sibling element declaration.
879                        // Local elements cannot be substitution heads (members
880                        // must be globally declared per §3.3.3), so this only
881                        // applies to refs.
882                        if let Some(head_key) = self
883                            .schema_set
884                            .lookup_element(ref_name.namespace, ref_name.local_name)
885                        {
886                            collect_substitution_members(self.schema_set, head_key, &mut result);
887                        }
888                    } else if let Some(name) = elem.name {
889                        // Local element — resolve namespace through form/elementFormDefault
890                        let source = p.source.as_ref().or(elem.source.as_ref());
891                        let ns = self.effective_element_namespace(elem, source);
892                        result.push((ns, name));
893                    }
894                }
895                ParticleTerm::Group(group) => {
896                    if let Some(ref_name) = &group.ref_name {
897                        if let Some(key) = self.resolve_model_group_key(ref_name) {
898                            if let Some(data) = self.schema_set.arenas.get_model_group(key) {
899                                result.extend(self.collect_sibling_element_qnames_inner(
900                                    &data.particles,
901                                    depth + 1,
902                                ));
903                            }
904                        }
905                    }
906                }
907                ParticleTerm::Any(_) => {}
908            }
909        }
910        result
911    }
912}
913
914/// Push QNames of every element that may substitute for `head_key` into `out`,
915/// honoring the head's `block`/`final` restrictions and walking transitively
916/// through nested substitution chains. Used for `##definedSibling` expansion.
917fn collect_substitution_members(
918    schema_set: &SchemaSet,
919    head_key: ElementKey,
920    out: &mut Vec<(Option<NameId>, NameId)>,
921) {
922    // Resolve through ref to get the canonical head decl.
923    let head_key = schema_set
924        .arenas
925        .elements
926        .get(head_key)
927        .and_then(|e| e.resolved_ref)
928        .unwrap_or(head_key);
929    let mut visited = std::collections::HashSet::new();
930    let mut stack = vec![head_key];
931    while let Some(current) = stack.pop() {
932        if !visited.insert(current) {
933            continue;
934        }
935        for (member_key, member) in schema_set.arenas.elements.iter() {
936            if member_key == current {
937                continue;
938            }
939            if member.resolved_substitution_groups.contains(&current) {
940                if let Some(name) = member.name {
941                    let entry = (member.target_namespace, name);
942                    if !out.contains(&entry) {
943                        out.push(entry);
944                    }
945                }
946                stack.push(member_key);
947            }
948        }
949    }
950}
951
952/// Cap occurrence bounds for UPA checking.
953///
954/// UPA ambiguity is structural: it arises at iteration boundaries.
955/// `maxOccurs=2` is sufficient to expose any boundary ambiguity.
956/// This follows the approach used by Xerces-J, Saxon, and .NET.
957///
958/// See Sperberg-McQueen (2005): for determinism testing, `F{n,m}` can be
959/// replaced with `F{min(n,1), min(m,2)}` without affecting the result.
960#[cfg_attr(test, allow(dead_code))]
961pub(super) fn cap_for_upa(min: u32, max: Option<u32>) -> (u32, Option<u32>) {
962    match (min, max) {
963        // Dead particle: maxOccurs=0 means the particle is absent
964        (0, Some(0)) => (0, Some(0)),
965        // Already simple: no capping needed
966        (m, Some(1)) if m <= 1 => (m, Some(1)),
967        // Exact repeat (min == max > 1): cap to {2, 2}
968        (m, Some(mx)) if m == mx && m > 1 => (2, Some(2)),
969        // Optional with repetition: preserve optionality + iteration boundary
970        (0, _) => (0, Some(2)),
971        // Required with repetition: cap to {1, 2}
972        (_, _) => (min.min(1), Some(2)),
973    }
974}
975
976/// Compile a particle to an NFA table (convenience function)
977pub fn compile_particle(
978    schema_set: &SchemaSet,
979    particle: &ParticleResult,
980    target_namespace: Option<NameId>,
981) -> NfaCompileResult<NfaTable> {
982    let mut ctx = CompileContext::new(schema_set, target_namespace);
983    ctx.compile_particle(particle)
984}
985
986/// Compile a model group to an NFA table (convenience function)
987pub fn compile_model_group(
988    schema_set: &SchemaSet,
989    group: &ModelGroupDefResult,
990    target_namespace: Option<NameId>,
991) -> NfaCompileResult<NfaTable> {
992    let mut ctx = CompileContext::new(schema_set, target_namespace);
993    ctx.compile_model_group(group)
994}
995
996/// Detect an inline `xs:all` at the top level of a particle.
997///
998/// Returns the all-group's particles and source if the particle's term is a
999/// group with `compositor == All` and no `ref_name` (i.e., an inline definition,
1000/// not a named model group reference).
1001pub(crate) fn is_top_level_all_group(
1002    particle: &ParticleResult,
1003) -> Option<(&[ParticleResult], Option<&SourceRef>)> {
1004    if let ParticleTerm::Group(group) = &particle.term {
1005        if group.compositor == Some(Compositor::All) && group.ref_name.is_none() {
1006            return Some((&group.particles, group.source.as_ref()));
1007        }
1008    }
1009    None
1010}
1011
1012/// Detect a named model group reference at the top level that resolves to
1013/// an `xs:all` group.
1014///
1015/// Returns the resolved [`ModelGroupData`] when the particle's term is a
1016/// group with a `ref_name` and the referenced definition has
1017/// `compositor == All`.
1018pub(crate) fn resolve_top_level_all_group_ref<'a>(
1019    particle: &ParticleResult,
1020    schema_set: &'a SchemaSet,
1021) -> Option<&'a ModelGroupData> {
1022    if let ParticleTerm::Group(group) = &particle.term {
1023        let ref_name = group.ref_name.as_ref()?;
1024        let group_key = schema_set.lookup_model_group(ref_name.namespace, ref_name.local_name)?;
1025        let group_data = schema_set.arenas.get_model_group(group_key)?;
1026        if group_data.compositor == Some(Compositor::All) {
1027            return Some(group_data);
1028        }
1029    }
1030    None
1031}
1032
1033/// Validate the outer occurrence constraints on a particle whose term is an all-group.
1034///
1035/// XSD 1.0 (cos-all-limited.2): minOccurs must be 0 or 1, maxOccurs must be 1.
1036/// XSD 1.1: more relaxed, but minOccurs > maxOccurs is always invalid.
1037pub(crate) fn validate_outer_all_group_occurs(
1038    particle: &ParticleResult,
1039    xsd_version: XsdVersion,
1040) -> NfaCompileResult<()> {
1041    let min = particle.min_occurs;
1042    let max = particle.max_occurs; // Option<u32>, None means unbounded
1043
1044    // XSD 1.0 specific constraints (check first for more specific error messages)
1045    if xsd_version == XsdVersion::V1_0 {
1046        if min > 1 {
1047            return Err(NfaCompileError::InvalidAllGroupOccurs {
1048                reason: format!(
1049                    "cos-all-limited.2: minOccurs must be 0 or 1 for xs:all group, found {}",
1050                    min
1051                ),
1052                location: particle.source.clone(),
1053            });
1054        }
1055        match max {
1056            Some(1) => {} // OK
1057            Some(n) => {
1058                return Err(NfaCompileError::InvalidAllGroupOccurs {
1059                    reason: format!(
1060                        "cos-all-limited.2: maxOccurs must be 1 for xs:all group, found {}",
1061                        n
1062                    ),
1063                    location: particle.source.clone(),
1064                });
1065            }
1066            None => {
1067                return Err(NfaCompileError::InvalidAllGroupOccurs {
1068                    reason: "cos-all-limited.2: maxOccurs='unbounded' not allowed for xs:all group"
1069                        .to_string(),
1070                    location: particle.source.clone(),
1071                });
1072            }
1073        }
1074    }
1075
1076    // Universal: minOccurs > maxOccurs is always invalid
1077    if let Some(max_val) = max {
1078        if min > max_val {
1079            return Err(NfaCompileError::InvalidOccurrence {
1080                min,
1081                max: max_val,
1082                location: particle.source.clone(),
1083            });
1084        }
1085    }
1086
1087    Ok(())
1088}
1089
1090/// Compile the base type's all-group model for an extension type.
1091///
1092/// If the extension's resolved base type is a complex type whose content model
1093/// compiles to an `AllGroup`, returns the `AllGroupModel`. Otherwise returns `None`.
1094#[cfg(feature = "xsd11")]
1095fn compile_base_all_group(
1096    schema_set: &SchemaSet,
1097    type_def: &ComplexTypeDefData,
1098) -> NfaCompileResult<Option<AllGroupModel>> {
1099    let base_ct_key = match type_def.resolved_base_type {
1100        Some(TypeKey::Complex(key)) => key,
1101        _ => return Ok(None),
1102    };
1103    let base_type_def = &schema_set.arenas.complex_types[base_ct_key];
1104    let base_matcher = compile_content_model_matcher(schema_set, base_type_def)?;
1105    match base_matcher {
1106        ContentModelMatcher::AllGroup(model) => Ok(Some(model)),
1107        _ => Ok(None),
1108    }
1109}
1110
1111/// Recognize the XSD 1.0 empty-base xs:all + group-ref-to-all extension
1112/// shape (mgO007, mgZ003) and produce its AllGroup matcher. Returns `None`
1113/// when the shape doesn't apply, leaving the caller to fall through.
1114#[cfg(feature = "xsd11")]
1115fn try_xsd10_empty_base_all_extension(
1116    ctx: &mut CompileContext<'_>,
1117    schema_set: &SchemaSet,
1118    type_def: &ComplexTypeDefData,
1119    is_extension: bool,
1120) -> NfaCompileResult<Option<ContentModelMatcher>> {
1121    if !is_extension || !schema_set.is_xsd10() {
1122        return Ok(None);
1123    }
1124    let Some(base_all_model) = compile_base_all_group(schema_set, type_def)? else {
1125        return Ok(None);
1126    };
1127    if !base_all_model.particles.is_empty() {
1128        return Ok(None);
1129    }
1130    let ComplexContentResult::Complex(def) = &type_def.content else {
1131        return Ok(None);
1132    };
1133    let Some(particle) = def.particle.as_ref() else {
1134        return Ok(None);
1135    };
1136    let Some(group_data) = resolve_top_level_all_group_ref(particle, schema_set) else {
1137        return Ok(None);
1138    };
1139    Ok(Some(compile_top_level_all_group_ref_matcher(
1140        ctx, schema_set, type_def, particle, group_data,
1141    )?))
1142}
1143
1144/// Convert an `AllGroupModel` into an NFA table for concatenation with
1145/// extension content. Each particle becomes a choice alternative wrapped
1146/// in repeat(0, max_occurs).
1147fn all_group_to_nfa(model: &AllGroupModel) -> NfaTable {
1148    let builder = FragmentBuilder::new();
1149    if model.particles.is_empty() {
1150        return fragment_to_table(builder.epsilon_fragment());
1151    }
1152
1153    let fragments: Vec<NfaFragment> = model
1154        .particles
1155        .iter()
1156        .map(|p| {
1157            let frag = builder.single_term(p.term.clone(), p.source.clone());
1158            let max = match p.max_occurs {
1159                MaxOccurs::Bounded(n) => Some(n),
1160                MaxOccurs::Unbounded => None,
1161            };
1162            apply_occurs(frag, p.min_occurs, MaxOccurs::from_option(max))
1163        })
1164        .collect();
1165
1166    let mut choice = fragments.into_iter().reduce(|a, b| a.alternate(b)).unwrap();
1167    let n = model.particles.len() as u32;
1168    choice = choice.repeat_range(0, Some(n));
1169
1170    fragment_to_table(choice)
1171}
1172
1173/// Build the content-model matcher for a complex type whose top-level
1174/// particle resolves (via `resolve_top_level_all_group_ref`) to a named
1175/// `xs:all` group. Validates outer occurrence constraints, sets up the
1176/// redefine redirect when applicable, compiles the all-group's particles,
1177/// and attaches open content.
1178fn compile_top_level_all_group_ref_matcher(
1179    ctx: &mut CompileContext<'_>,
1180    schema_set: &SchemaSet,
1181    type_def: &ComplexTypeDefData,
1182    particle: &ParticleResult,
1183    group_data: &ModelGroupData,
1184) -> NfaCompileResult<ContentModelMatcher> {
1185    validate_outer_all_group_occurs(particle, schema_set.xsd_version)?;
1186    ctx.resolved_particle_types = group_data.resolved_particle_types.clone();
1187    ctx.resolved_particle_elements = group_data.resolved_particle_elements.clone();
1188    ctx.content_flat_idx = Some(0);
1189    // Self-references inside the all-group resolve to the original group,
1190    // not back to the redefining group.
1191    if let (Some(original_key), Some(name)) = (group_data.redefine_original, group_data.name) {
1192        ctx.redefine_redirect = Some((name, group_data.target_namespace, original_key));
1193    }
1194    let mut model = ctx.compile_all_group_model(&group_data.particles, group_data.source.as_ref())?;
1195    if particle.min_occurs == 0 {
1196        model.outer_optional = true;
1197    }
1198    let base_matcher = ContentModelMatcher::AllGroup(model);
1199    let open_content = resolve_open_content(
1200        schema_set,
1201        &type_def.content,
1202        type_def.open_content.as_ref(),
1203        type_def.source.as_ref(),
1204    );
1205    Ok(attach_open_content(schema_set, base_matcher, open_content))
1206}
1207
1208/// Compile a complex type's content model into a matcher, applying open content defaults.
1209pub fn compile_content_model_matcher(
1210    schema_set: &SchemaSet,
1211    type_def: &ComplexTypeDefData,
1212) -> NfaCompileResult<ContentModelMatcher> {
1213    compile_content_model_matcher_impl(schema_set, type_def, false)
1214}
1215
1216/// Compile a content model with capped occurrence bounds for UPA checking.
1217///
1218/// All `maxOccurs` values are reduced to at most 2 before NFA construction,
1219/// producing a counter-free NFA suitable for epsilon-closure-based UPA analysis.
1220/// This is the standard approach used by Xerces-J, Saxon, and .NET.
1221pub fn compile_content_model_for_upa(
1222    schema_set: &SchemaSet,
1223    type_def: &ComplexTypeDefData,
1224) -> NfaCompileResult<ContentModelMatcher> {
1225    compile_content_model_matcher_impl(schema_set, type_def, true)
1226}
1227
1228fn compile_content_model_matcher_impl(
1229    schema_set: &SchemaSet,
1230    type_def: &ComplexTypeDefData,
1231    upa_mode: bool,
1232) -> NfaCompileResult<ContentModelMatcher> {
1233    let target_namespace = type_def.target_namespace;
1234    let mut ctx = if upa_mode {
1235        CompileContext::new_for_upa(schema_set, target_namespace)
1236    } else {
1237        CompileContext::new(schema_set, target_namespace)
1238    };
1239    let is_extension = matches!(
1240        type_def.derivation_method,
1241        Some(DerivationMethod::Extension)
1242    );
1243
1244    // Try the all-group path for non-extension types with an inline xs:all
1245    if !is_extension {
1246        if let ComplexContentResult::Complex(def) = &type_def.content {
1247            if let Some(particle) = &def.particle {
1248                if let Some((all_particles, all_source)) = is_top_level_all_group(particle) {
1249                    validate_outer_all_group_occurs(particle, schema_set.xsd_version)?;
1250                    ctx.resolved_particle_types = type_def.resolved_content_particle_types.to_vec();
1251                    ctx.resolved_particle_elements =
1252                        type_def.resolved_content_particle_elements.to_vec();
1253                    ctx.content_flat_idx = Some(0);
1254                    let mut model = ctx.compile_all_group_model(all_particles, all_source)?;
1255                    if particle.min_occurs == 0 {
1256                        model.outer_optional = true;
1257                    }
1258                    let base_matcher = ContentModelMatcher::AllGroup(model);
1259
1260                    let open_content = resolve_open_content(
1261                        schema_set,
1262                        &type_def.content,
1263                        type_def.open_content.as_ref(),
1264                        type_def.source.as_ref(),
1265                    );
1266
1267                    return Ok(attach_open_content(schema_set, base_matcher, open_content));
1268                }
1269
1270                // Named group ref resolving to all-group
1271                if let Some(group_data) = resolve_top_level_all_group_ref(particle, schema_set) {
1272                    return compile_top_level_all_group_ref_matcher(
1273                        &mut ctx, schema_set, type_def, particle, group_data,
1274                    );
1275                }
1276            }
1277        }
1278    }
1279
1280    // XSD 1.0: empty-base xs:all extended via a top-level group reference to
1281    // an xs:all. Without this branch the xsd11-feature path below would
1282    // reject the schema even when the SchemaSet runs in XSD 1.0 mode
1283    // (mgO007, mgZ003). Per §3.4.2.2 / §3.8 cos-all-limited, the empty base
1284    // contributes nothing so the result is the extension's own all-group.
1285    #[cfg(feature = "xsd11")]
1286    if let Some(matcher) =
1287        try_xsd10_empty_base_all_extension(&mut ctx, schema_set, type_def, is_extension)?
1288    {
1289        return Ok(matcher);
1290    }
1291
1292    // XSD 1.1: Extension from an all-group base type — produce AllGroup or
1293    // AllGroupExtension instead of the lossy NFA conversion.
1294    #[cfg(feature = "xsd11")]
1295    if is_extension && schema_set.is_xsd11() {
1296        if let Some(base_all_model) = compile_base_all_group(schema_set, type_def)? {
1297            let open_content = resolve_open_content(
1298                schema_set,
1299                &type_def.content,
1300                type_def.open_content.as_ref(),
1301                type_def.source.as_ref(),
1302            );
1303
1304            // Determine what the extension adds
1305            let own_particle = match &type_def.content {
1306                ComplexContentResult::Complex(def) => def.particle.as_ref(),
1307                _ => None,
1308            };
1309
1310            match own_particle {
1311                None => {
1312                    // Extension adds only attributes — return base AllGroup directly
1313                    let matcher = ContentModelMatcher::AllGroup(base_all_model);
1314                    return Ok(attach_open_content(schema_set, matcher, open_content));
1315                }
1316                Some(particle) => {
1317                    // Check if extension's own particle is an inline all-group
1318                    if let Some((ext_particles, ext_source)) = is_top_level_all_group(particle) {
1319                        // §3.4.2.3 / cos-ct-extends: when both base and extension
1320                        // are all-groups, their outer {min occurs} must match.
1321                        let base_outer_optional = base_all_model.outer_optional;
1322                        let ext_outer_optional = particle.min_occurs == 0;
1323                        if base_outer_optional != ext_outer_optional {
1324                            return Err(NfaCompileError::InvalidAllGroupOccurs {
1325                                reason: format!(
1326                                    "cos-ct-extends: when extending an xs:all base with an xs:all, \
1327                                     the outer minOccurs must match (base minOccurs={}, \
1328                                     extension minOccurs={})",
1329                                    if base_outer_optional { 0 } else { 1 },
1330                                    particle.min_occurs,
1331                                ),
1332                                location: particle.source.clone().or_else(|| ext_source.cloned()),
1333                            });
1334                        }
1335
1336                        // Reject extending an empty xs:all — there is no
1337                        // base content to extend and the resulting type would
1338                        // collapse to the extension's own all-group, which is
1339                        // not a true extension. (W3C bug 6202; Saxon allows
1340                        // this but conformance tests treat it as invalid.)
1341                        if base_all_model.particles.is_empty() {
1342                            return Err(NfaCompileError::InvalidAllGroupContent {
1343                                location: particle.source.clone().or_else(|| ext_source.cloned()),
1344                            });
1345                        }
1346
1347                        let mut ctx = if upa_mode {
1348                            CompileContext::new_for_upa(schema_set, type_def.target_namespace)
1349                        } else {
1350                            CompileContext::new(schema_set, type_def.target_namespace)
1351                        };
1352                        ctx.resolved_particle_types =
1353                            type_def.resolved_content_particle_types.to_vec();
1354                        ctx.resolved_particle_elements =
1355                            type_def.resolved_content_particle_elements.to_vec();
1356                        ctx.content_flat_idx = Some(0);
1357                        let ext_model = ctx.compile_all_group_model(ext_particles, ext_source)?;
1358
1359                        let merged_outer_optional = base_outer_optional && ext_outer_optional;
1360                        let mut merged_particles = base_all_model.particles;
1361                        merged_particles.extend(ext_model.particles);
1362                        let mut merged = AllGroupModel::new(merged_particles);
1363                        merged.outer_optional = merged_outer_optional;
1364                        let matcher = ContentModelMatcher::AllGroup(merged);
1365                        return Ok(attach_open_content(schema_set, matcher, open_content));
1366                    }
1367
1368                    // Extension is sequence/choice/group-ref — invalid per
1369                    // cos-all-limited.1.2: an xs:all may only appear at the top
1370                    // of a content model. Wrapping the base's all-group inside
1371                    // a sequence(base, extension) violates that constraint.
1372                    return Err(NfaCompileError::InvalidAllGroupContent {
1373                        location: particle.source.clone(),
1374                    });
1375                }
1376            }
1377        }
1378    }
1379
1380    // Standard NFA path (sequences, choices, named group refs, extensions)
1381    let own_nfa = match &type_def.content {
1382        ComplexContentResult::Complex(def) => match &def.particle {
1383            Some(particle) => {
1384                ctx.resolved_particle_types = type_def.resolved_content_particle_types.to_vec();
1385                ctx.resolved_particle_elements =
1386                    type_def.resolved_content_particle_elements.to_vec();
1387                ctx.content_flat_idx = Some(0);
1388                Some(ctx.compile_particle(particle)?)
1389            }
1390            None => None,
1391        },
1392        ComplexContentResult::Empty | ComplexContentResult::Simple(_) => None,
1393    };
1394
1395    // For extensions, prepend the base type's content model. Capture base's effective
1396    // OC for §3.4.2.3 clause 6 inheritance/union on XSD 1.1.
1397    #[cfg(feature = "xsd11")]
1398    let mut inherited_oc: Option<OpenContent> = None;
1399    #[cfg(feature = "xsd11")]
1400    let mut base_target_ns: Option<NameId> = None;
1401
1402    let base_nfa = if is_extension {
1403        if let Some(TypeKey::Complex(base_ct_key)) = type_def.resolved_base_type {
1404            let base_type_def = &schema_set.arenas.complex_types[base_ct_key];
1405            #[cfg(feature = "xsd11")]
1406            {
1407                base_target_ns = base_type_def.target_namespace;
1408            }
1409            let base_matcher =
1410                compile_content_model_matcher_impl(schema_set, base_type_def, upa_mode)?;
1411            match base_matcher {
1412                ContentModelMatcher::Nfa(nfa) => Some(nfa),
1413                ContentModelMatcher::WithOpenContent {
1414                    nfa,
1415                    mode,
1416                    wildcard,
1417                } => {
1418                    #[cfg(feature = "xsd11")]
1419                    {
1420                        inherited_oc = Some(OpenContent {
1421                            mode,
1422                            wildcard,
1423                            source: None,
1424                        });
1425                    }
1426                    #[cfg(not(feature = "xsd11"))]
1427                    let _ = (mode, wildcard);
1428                    Some(nfa)
1429                }
1430                ContentModelMatcher::AllGroup(ref model) => {
1431                    if own_nfa.is_none() {
1432                        // Extension adds only attributes — base AllGroup already carries its OC;
1433                        // attach_open_content(AllGroup, None) preserves it.
1434                        let open_content = resolve_open_content(
1435                            schema_set,
1436                            &type_def.content,
1437                            type_def.open_content.as_ref(),
1438                            type_def.source.as_ref(),
1439                        );
1440                        return Ok(attach_open_content(schema_set, base_matcher, open_content));
1441                    }
1442                    // Extension has own particles — convert AllGroup to NFA for concat
1443                    // (XSD 1.0 path; XSD 1.1 is handled above via compile_base_all_group)
1444                    Some(all_group_to_nfa(model))
1445                }
1446                #[cfg(feature = "xsd11")]
1447                ContentModelMatcher::AllGroupExtension { .. } => {
1448                    // Should not occur: base type should not produce AllGroupExtension
1449                    unreachable!("base type produced AllGroupExtension")
1450                }
1451            }
1452        } else {
1453            None
1454        }
1455    } else {
1456        None
1457    };
1458
1459    let effective_nfa = match (base_nfa, own_nfa) {
1460        (Some(base), Some(own)) => base.concat(own),
1461        (Some(base), None) => base,
1462        (None, Some(own)) => own,
1463        (None, None) => empty_nfa(),
1464    };
1465
1466    let base_matcher = ContentModelMatcher::Nfa(effective_nfa);
1467
1468    // §3.4.2.3 clause 6 (inherit + union) for XSD 1.1 extensions; simple resolve otherwise.
1469    #[cfg(feature = "xsd11")]
1470    let open_content = if schema_set.is_xsd11() && is_extension {
1471        effective_open_content_for_extension(
1472            schema_set,
1473            type_def,
1474            base_target_ns,
1475            inherited_oc.as_ref(),
1476        )
1477    } else {
1478        resolve_open_content(
1479            schema_set,
1480            &type_def.content,
1481            type_def.open_content.as_ref(),
1482            type_def.source.as_ref(),
1483        )
1484    };
1485    #[cfg(not(feature = "xsd11"))]
1486    let open_content = resolve_open_content(
1487        schema_set,
1488        &type_def.content,
1489        type_def.open_content.as_ref(),
1490        type_def.source.as_ref(),
1491    );
1492
1493    Ok(attach_open_content(schema_set, base_matcher, open_content))
1494}
1495
1496/// §3.4.2.3 clauses 5–6: effective open content for an XSD 1.1 extension type.
1497#[cfg(feature = "xsd11")]
1498fn effective_open_content_for_extension(
1499    schema_set: &SchemaSet,
1500    type_def: &ComplexTypeDefData,
1501    base_target_ns: Option<NameId>,
1502    inherited: Option<&OpenContent>,
1503) -> Option<OpenContent> {
1504    let own_oc = resolve_open_content(
1505        schema_set,
1506        &type_def.content,
1507        type_def.open_content.as_ref(),
1508        type_def.source.as_ref(),
1509    );
1510    match own_oc {
1511        // Clause 6.1: own OC absent or mode="none" → inherit base OC.
1512        None => inherited.cloned(),
1513        // Clause 6.2: union wildcards with base OC.
1514        Some(own) => {
1515            let Some(base_oc) = inherited else {
1516                return Some(own);
1517            };
1518            let derived_target_ns = type_def.target_namespace;
1519            let unioned_wildcard = match (own.wildcard.as_ref(), base_oc.wildcard.as_ref()) {
1520                (Some(own_wc), Some(base_wc)) => Some(wildcard_ref_union(
1521                    base_wc,
1522                    base_target_ns,
1523                    own_wc,
1524                    derived_target_ns,
1525                )),
1526                (Some(own_wc), None) => Some(own_wc.clone()),
1527                (None, Some(base_wc)) => Some(base_wc.clone()),
1528                (None, None) => None,
1529            };
1530            Some(OpenContent {
1531                mode: own.mode,
1532                wildcard: unioned_wildcard,
1533                source: own.source,
1534            })
1535        }
1536    }
1537}
1538
1539/// §3.10.6.3 cos-aw-union on `WildcardRef`.
1540#[cfg(feature = "xsd11")]
1541fn wildcard_ref_union(
1542    base: &WildcardRef,
1543    base_target_ns: Option<NameId>,
1544    derived: &WildcardRef,
1545    derived_target_ns: Option<NameId>,
1546) -> WildcardRef {
1547    let c1 = expand_ns_constraint(&base.namespace_constraint, base_target_ns);
1548    let c2 = expand_ns_constraint(&derived.namespace_constraint, derived_target_ns);
1549    let union_ns = namespace_constraint_union(c1, c2);
1550
1551    let process_contents =
1552        less_restrictive_process_contents(base.process_contents, derived.process_contents);
1553
1554    // notQName union: exclusion requires both sides to exclude.
1555    let not_qnames: Vec<_> = base
1556        .not_qnames
1557        .iter()
1558        .filter(|q| derived.not_qnames.contains(q))
1559        .cloned()
1560        .collect();
1561
1562    WildcardRef {
1563        namespace_constraint: union_ns,
1564        process_contents,
1565        not_qnames,
1566        has_defined_sibling: false,
1567        source: derived.source.clone(),
1568    }
1569}
1570
1571/// Expand token-form namespace constraints (Other/TargetNamespace/Local) to explicit sets.
1572#[cfg(feature = "xsd11")]
1573fn expand_ns_constraint(
1574    nc: &NamespaceConstraint,
1575    target_ns: Option<NameId>,
1576) -> NamespaceConstraint {
1577    match nc {
1578        NamespaceConstraint::Other => NamespaceConstraint::Not(vec![target_ns, None]),
1579        NamespaceConstraint::TargetNamespace => NamespaceConstraint::List(vec![target_ns]),
1580        NamespaceConstraint::Local => NamespaceConstraint::List(vec![None]),
1581        other => other.clone(),
1582    }
1583}
1584
1585/// §3.10.6.3 set union. Callers must pre-expand token forms via `expand_ns_constraint`.
1586#[cfg(feature = "xsd11")]
1587fn namespace_constraint_union(
1588    c1: NamespaceConstraint,
1589    c2: NamespaceConstraint,
1590) -> NamespaceConstraint {
1591    match (c1, c2) {
1592        // Any ∪ X = Any
1593        (NamespaceConstraint::Any, _) | (_, NamespaceConstraint::Any) => NamespaceConstraint::Any,
1594        // Not(E1) ∪ Not(E2) = Not(E1 ∩ E2)
1595        (NamespaceConstraint::Not(e1), NamespaceConstraint::Not(e2)) => {
1596            let intersection: Vec<_> = e1.iter().filter(|x| e2.contains(x)).cloned().collect();
1597            if intersection.is_empty() {
1598                NamespaceConstraint::Any
1599            } else {
1600                NamespaceConstraint::Not(intersection)
1601            }
1602        }
1603        // Not(E) ∪ Pos(S) = Not(E \ S)  [and symmetric]
1604        (NamespaceConstraint::Not(e), NamespaceConstraint::List(s))
1605        | (NamespaceConstraint::List(s), NamespaceConstraint::Not(e)) => {
1606            let diff: Vec<_> = e.into_iter().filter(|x| !s.contains(x)).collect();
1607            if diff.is_empty() {
1608                NamespaceConstraint::Any
1609            } else {
1610                NamespaceConstraint::Not(diff)
1611            }
1612        }
1613        // Pos(S1) ∪ Pos(S2) = Pos(S1 ∪ S2)
1614        (NamespaceConstraint::List(mut a), NamespaceConstraint::List(b)) => {
1615            for x in b {
1616                if !a.contains(&x) {
1617                    a.push(x);
1618                }
1619            }
1620            NamespaceConstraint::List(a)
1621        }
1622        // Token forms should be pre-expanded; widen to Any defensively.
1623        (
1624            NamespaceConstraint::Other
1625            | NamespaceConstraint::TargetNamespace
1626            | NamespaceConstraint::Local,
1627            _,
1628        )
1629        | (
1630            _,
1631            NamespaceConstraint::Other
1632            | NamespaceConstraint::TargetNamespace
1633            | NamespaceConstraint::Local,
1634        ) => NamespaceConstraint::Any,
1635    }
1636}
1637
1638/// Return the less-restrictive of two processContents values (skip > lax > strict).
1639#[cfg(feature = "xsd11")]
1640fn less_restrictive_process_contents(
1641    a: TypesProcessContents,
1642    b: TypesProcessContents,
1643) -> TypesProcessContents {
1644    match (a, b) {
1645        (TypesProcessContents::Skip, _) | (_, TypesProcessContents::Skip) => {
1646            TypesProcessContents::Skip
1647        }
1648        (TypesProcessContents::Lax, _) | (_, TypesProcessContents::Lax) => {
1649            TypesProcessContents::Lax
1650        }
1651        _ => TypesProcessContents::Strict,
1652    }
1653}
1654
1655fn empty_nfa() -> NfaTable {
1656    let builder = FragmentBuilder::new();
1657    fragment_to_table(builder.epsilon_fragment())
1658}
1659
1660fn attach_open_content(
1661    schema_set: &SchemaSet,
1662    matcher: ContentModelMatcher,
1663    open_content: Option<OpenContent>,
1664) -> ContentModelMatcher {
1665    let open_content = match open_content {
1666        Some(open_content) => open_content,
1667        None => return matcher,
1668    };
1669
1670    match matcher {
1671        ContentModelMatcher::Nfa(nfa) => {
1672            let wildcard = open_content.wildcard.map(|mut w| {
1673                if w.has_defined_sibling {
1674                    w.not_qnames
1675                        .extend(collect_nfa_element_qnames(schema_set, &nfa));
1676                    w.has_defined_sibling = false;
1677                }
1678                w
1679            });
1680            ContentModelMatcher::WithOpenContent {
1681                nfa,
1682                mode: open_content.mode,
1683                wildcard,
1684            }
1685        }
1686        ContentModelMatcher::AllGroup(mut model) => {
1687            if let Some(mut wildcard_ref) = open_content.wildcard {
1688                if wildcard_ref.has_defined_sibling {
1689                    wildcard_ref
1690                        .not_qnames
1691                        .extend(collect_all_group_element_qnames(schema_set, &model));
1692                    wildcard_ref.has_defined_sibling = false;
1693                }
1694                let mode = match open_content.mode {
1695                    TypesOpenContentMode::Interleave => AllGroupOpenContentMode::Interleave,
1696                    TypesOpenContentMode::Suffix => AllGroupOpenContentMode::Suffix,
1697                    TypesOpenContentMode::None => AllGroupOpenContentMode::None,
1698                };
1699                model.open_content = Some(OpenContentWildcard {
1700                    namespace_constraint: wildcard_ref.namespace_constraint,
1701                    process_contents: wildcard_ref.process_contents,
1702                    mode,
1703                    not_qnames: wildcard_ref.not_qnames,
1704                });
1705            }
1706            ContentModelMatcher::AllGroup(model)
1707        }
1708        #[cfg(feature = "xsd11")]
1709        ContentModelMatcher::AllGroupExtension {
1710            mut base_model,
1711            extension_nfa,
1712        } => {
1713            if let Some(mut wildcard_ref) = open_content.wildcard {
1714                if wildcard_ref.has_defined_sibling {
1715                    // Collect siblings from both the base all-group and extension NFA
1716                    wildcard_ref
1717                        .not_qnames
1718                        .extend(collect_all_group_element_qnames(schema_set, &base_model));
1719                    wildcard_ref
1720                        .not_qnames
1721                        .extend(collect_nfa_element_qnames(schema_set, &extension_nfa));
1722                    wildcard_ref.has_defined_sibling = false;
1723                }
1724                let mode = match open_content.mode {
1725                    TypesOpenContentMode::Interleave => AllGroupOpenContentMode::Interleave,
1726                    TypesOpenContentMode::Suffix => AllGroupOpenContentMode::Suffix,
1727                    TypesOpenContentMode::None => AllGroupOpenContentMode::None,
1728                };
1729                base_model.open_content = Some(OpenContentWildcard {
1730                    namespace_constraint: wildcard_ref.namespace_constraint,
1731                    process_contents: wildcard_ref.process_contents,
1732                    mode,
1733                    not_qnames: wildcard_ref.not_qnames,
1734                });
1735            }
1736            ContentModelMatcher::AllGroupExtension {
1737                base_model,
1738                extension_nfa,
1739            }
1740        }
1741        other => other,
1742    }
1743}
1744
1745fn resolve_open_content(
1746    schema_set: &SchemaSet,
1747    content: &ComplexContentResult,
1748    explicit: Option<&OpenContentResult>,
1749    source: Option<&SourceRef>,
1750) -> Option<OpenContent> {
1751    if !schema_set.is_xsd11() {
1752        return None;
1753    }
1754
1755    if let Some(explicit) = explicit {
1756        let target_namespace = source
1757            .and_then(|s| schema_set.documents.get(s.doc_id as usize))
1758            .and_then(|d| d.target_namespace);
1759        return open_content_from_result(explicit, schema_set, target_namespace);
1760    }
1761
1762    if !matches!(
1763        content,
1764        ComplexContentResult::Complex(_) | ComplexContentResult::Empty
1765    ) {
1766        return None;
1767    }
1768
1769    // Use defaults_doc() so components that were moved into an xs:override
1770    // read the overridden schema document's <xs:defaultOpenContent> — per
1771    // §4.2.5 and the saxon open043 test ("For types defined within xs:override,
1772    // the relevant defaultOpenContent is the one in the overridden schema
1773    // document").  For non-override components defaults_doc() == doc_id so
1774    // normal parsing is unchanged.
1775    let doc = source.and_then(|s| schema_set.documents.get(s.defaults_doc() as usize));
1776    let default = doc.and_then(|d| d.default_open_content.as_ref())?;
1777
1778    // §3.4.2.3 step 5.2.2: defaultOpenContent only applies to a type whose
1779    // explicit content type variety = empty when appliesToEmpty=true.
1780    // "Variety = empty" requires *both* an empty explicit content (step 2)
1781    // AND effective mixed = false (step 3.1.2). When mixed=true, an empty
1782    // explicit content promotes to a non-empty mixed content type, so
1783    // appliesToEmpty=false should still attach the OC.
1784    if !default.applies_to_empty && content.explicit_content_type_is_empty() {
1785        return None;
1786    }
1787
1788    open_content_from_default(default, schema_set)
1789}
1790
1791fn open_content_from_result(
1792    result: &OpenContentResult,
1793    schema_set: &SchemaSet,
1794    target_namespace: Option<NameId>,
1795) -> Option<OpenContent> {
1796    let mode: TypesOpenContentMode = result.mode.into();
1797    if matches!(mode, TypesOpenContentMode::None) {
1798        return None;
1799    }
1800
1801    Some(OpenContent {
1802        mode,
1803        wildcard: result
1804            .wildcard
1805            .as_ref()
1806            .map(|w| wildcard_ref_from_result(w, schema_set, target_namespace)),
1807        source: result.source.clone(),
1808    })
1809}
1810
1811fn open_content_from_default(
1812    default: &DefaultOpenContent,
1813    schema_set: &SchemaSet,
1814) -> Option<OpenContent> {
1815    let mode: TypesOpenContentMode = default.mode.into();
1816    if matches!(mode, TypesOpenContentMode::None) {
1817        return None;
1818    }
1819
1820    Some(OpenContent {
1821        mode,
1822        wildcard: default
1823            .wildcard
1824            .as_ref()
1825            .map(|w| wildcard_ref_from_default(w, schema_set)),
1826        source: default.source.clone(),
1827    })
1828}
1829
1830/// Expand all globally declared element QNames from the schema set.
1831fn expand_defined_element_qnames(schema_set: &SchemaSet) -> Vec<(Option<NameId>, NameId)> {
1832    schema_set
1833        .namespaces
1834        .iter()
1835        .flat_map(|(ns, table)| table.elements.keys().map(move |name| (*ns, *name)))
1836        .collect()
1837}
1838
1839/// Collect all element QNames from an NFA content model (for ##definedSibling
1840/// expansion). Includes substitution-group members of each declared element.
1841fn collect_nfa_element_qnames(
1842    schema_set: &SchemaSet,
1843    nfa: &NfaTable,
1844) -> Vec<(Option<NameId>, NameId)> {
1845    let mut result = Vec::new();
1846    for state in &nfa.states {
1847        if let Some(NfaTerm::Element {
1848            namespace,
1849            name,
1850            element_key,
1851            ..
1852        }) = &state.term
1853        {
1854            let qname = (*namespace, *name);
1855            if !result.contains(&qname) {
1856                result.push(qname);
1857            }
1858            if let Some(head_key) = element_key {
1859                collect_substitution_members(schema_set, *head_key, &mut result);
1860            }
1861        }
1862    }
1863    result
1864}
1865
1866/// Collect all element QNames from an all-group model (for ##definedSibling
1867/// expansion). Includes substitution-group members of each declared element.
1868fn collect_all_group_element_qnames(
1869    schema_set: &SchemaSet,
1870    model: &AllGroupModel,
1871) -> Vec<(Option<NameId>, NameId)> {
1872    let mut result = Vec::new();
1873    for particle in &model.particles {
1874        if let NfaTerm::Element {
1875            namespace,
1876            name,
1877            element_key,
1878            ..
1879        } = &particle.term
1880        {
1881            let qname = (*namespace, *name);
1882            if !result.contains(&qname) {
1883                result.push(qname);
1884            }
1885            if let Some(head_key) = element_key {
1886                collect_substitution_members(schema_set, *head_key, &mut result);
1887            }
1888        }
1889    }
1890    result
1891}
1892
1893fn wildcard_ref_from_result(
1894    wildcard: &WildcardResult,
1895    schema_set: &SchemaSet,
1896    target_namespace: Option<NameId>,
1897) -> WildcardRef {
1898    let mut namespace_constraint = match &wildcard.namespace {
1899        WildcardNamespace::Any => NamespaceConstraint::Any,
1900        WildcardNamespace::Other => NamespaceConstraint::Other,
1901        WildcardNamespace::TargetNamespace => NamespaceConstraint::TargetNamespace,
1902        WildcardNamespace::Local => NamespaceConstraint::Local,
1903        WildcardNamespace::List(list) => {
1904            NamespaceConstraint::List(list.iter().map(|t| t.resolve(target_namespace)).collect())
1905        }
1906    };
1907
1908    // Override with notNamespace if present
1909    if !wildcard.not_namespace.is_empty() {
1910        let excluded: Vec<Option<NameId>> = wildcard
1911            .not_namespace
1912            .iter()
1913            .map(|t| t.resolve(target_namespace))
1914            .collect();
1915        namespace_constraint = NamespaceConstraint::Not(excluded);
1916    }
1917
1918    // Expand notQName — resolve ##defined to concrete QNames using schema_set
1919    let mut not_qnames: Vec<(Option<NameId>, NameId)> = Vec::new();
1920    let mut has_defined_sibling = false;
1921    for item in &wildcard.not_qname {
1922        match item {
1923            NotQNameItem::QName {
1924                namespace,
1925                local_name,
1926            } => {
1927                not_qnames.push((*namespace, *local_name));
1928            }
1929            NotQNameItem::Defined => {
1930                not_qnames.extend(expand_defined_element_qnames(schema_set));
1931            }
1932            NotQNameItem::DefinedSibling => {
1933                // Defer: sibling context not yet available for open content wildcards.
1934                // Resolved in attach_open_content when siblings are known.
1935                has_defined_sibling = true;
1936            }
1937        }
1938    }
1939
1940    let process_contents = match wildcard.process_contents {
1941        ProcessContents::Strict => TypesProcessContents::Strict,
1942        ProcessContents::Lax => TypesProcessContents::Lax,
1943        ProcessContents::Skip => TypesProcessContents::Skip,
1944    };
1945
1946    WildcardRef {
1947        namespace_constraint,
1948        process_contents,
1949        not_qnames,
1950        has_defined_sibling,
1951        source: wildcard.source.clone(),
1952    }
1953}
1954
1955fn wildcard_ref_from_default(wildcard: &ElementWildcard, schema_set: &SchemaSet) -> WildcardRef {
1956    let namespace_constraint = match &wildcard.namespace_constraint {
1957        SchemaNamespaceConstraint::Any => NamespaceConstraint::Any,
1958        SchemaNamespaceConstraint::Other => NamespaceConstraint::Other,
1959        SchemaNamespaceConstraint::Enumeration(list) => NamespaceConstraint::List(list.clone()),
1960        SchemaNamespaceConstraint::Not(excluded) => NamespaceConstraint::Not(excluded.clone()),
1961    };
1962
1963    // Expand not_qnames from ElementWildcard — resolve ##defined using schema_set
1964    let mut not_qnames: Vec<(Option<NameId>, NameId)> = Vec::new();
1965    let mut has_defined_sibling = false;
1966    for item in &wildcard.not_qnames {
1967        match item {
1968            crate::schema::wildcard::QNameDisallowed::QName {
1969                namespace,
1970                local_name,
1971            } => {
1972                not_qnames.push((*namespace, *local_name));
1973            }
1974            crate::schema::wildcard::QNameDisallowed::Defined => {
1975                not_qnames.extend(expand_defined_element_qnames(schema_set));
1976            }
1977            crate::schema::wildcard::QNameDisallowed::DefinedSibling => {
1978                // Defer: sibling context not yet available for open content wildcards.
1979                // Resolved in attach_open_content when siblings are known.
1980                has_defined_sibling = true;
1981            }
1982        }
1983    }
1984
1985    WildcardRef {
1986        namespace_constraint,
1987        process_contents: wildcard.process_contents,
1988        not_qnames,
1989        has_defined_sibling,
1990        source: wildcard.source.clone(),
1991    }
1992}
1993
1994#[cfg(test)]
1995#[path = "compile_tests.rs"]
1996mod tests;