extension PureXML.Schema { /// A transition label in the content-model automaton: a specific expanded /// element name, or a wildcard. enum TermLabel: Sendable { case name(PureXML.Model.QualifiedName) case wildcard(Wildcard) func matches(_ actual: PureXML.Model.QualifiedName) -> Bool { switch self { case let .wildcard(wildcard): wildcard.admits(actual) case let .name(declared): declared.localName == actual.localName && declared.namespaceURI == actual.namespaceURI } } } /// One state of the content-model automaton. struct ContentState: Sendable { var epsilon: [Int] = [] var label: TermLabel? var target: Int? } /// A Thompson NFA over element names compiled from a ``Particle``. `all` groups /// are validated separately by ``ContentMatcher`` and are not represented here. struct ContentNFA: Sendable { var states: [ContentState] var start: Int var accept: Int func matchesWhole(_ names: [PureXML.Model.QualifiedName]) -> Bool { var current = startStates() for name in names { guard let next = step(current, over: name) else { return false } current = next } return isComplete(current) } /// After consuming `names`, the labels the automaton can accept next and /// whether the content may legally end here. The follow-set: the exact set /// of element names allowed at this point, for completions and for telling /// whether something is still required. Returns an empty, not-complete /// result when the prefix is already invalid. func follow(after names: [PureXML.Model.QualifiedName]) -> (allowed: [TermLabel], complete: Bool) { var current = startStates() for name in names { guard let next = step(current, over: name) else { return ([], false) } current = next } return (admissible(from: current), isComplete(current)) } // MARK: Incremental matching // // The active set is the epsilon-closure of the states reachable after some // prefix of children. Callers that scan a child sequence advance it one // child at a time (`startStates` then `step` per child) rather than calling // `follow(after:)` on a growing prefix, which re-walks the whole prefix each // time and is quadratic in the child count over a content model (#129). /// The active set before any child: the closure of the start state. func startStates() -> Set { closure([start]) } /// The labels admissible from `current`, for diagnostics and completions. func admissible(from current: Set) -> [TermLabel] { var labels: [TermLabel] = [] for state in current where states[state].target != nil { if let label = states[state].label { labels.append(label) } } return labels } /// Advances `current` by consuming one element `name`, or nil when `name` /// is not admissible there. func step(_ current: Set, over name: PureXML.Model.QualifiedName) -> Set? { var next: Set = [] for state in current { if let label = states[state].label, let target = states[state].target, label.matches(name) { next.insert(target) } } if next.isEmpty { return nil } return closure(next) } /// Whether the content may legally end at `current`. func isComplete(_ current: Set) -> Bool { current.contains(accept) } private func closure(_ seed: Set) -> Set { var seen = seed var stack = Array(seed) while let state = stack.popLast() { for next in states[state].epsilon where seen.insert(next).inserted { stack.append(next) } } return seen } } /// Builds a ``ContentNFA`` from a particle by Thompson construction. struct ContentNFABuilder { private var states: [ContentState] = [] static func build(_ particle: PureXML.Schema.Particle) -> ContentNFA { var builder = ContentNFABuilder() let fragment = builder.particle(particle) return ContentNFA(states: builder.states, start: fragment.start, accept: fragment.accept) } private mutating func addState() -> Int { states.append(ContentState()) return states.count - 1 } /// Occurrence counts unroll into automaton states, so they are /// capped: beyond the cap a repetition is treated as unbounded, the /// libxml2 posture (its cap is 16384). Without this, a schema with /// maxOccurs in the trillions allocates until the process dies /// (found by XSTS groupF009v, #129). private static let occursUnrollCap = 16384 /// The per-particle cap bounds one repetition, but nested particles /// multiply: a sequence of 16384 holding a choice of 16384 is 2.7e8 /// states even though each particle is individually capped. So the /// total state count is also capped; once an NFA reaches the ceiling /// every further repetition degrades to star, the same posture the /// per-particle cap already takes. Legitimate content models are tens /// of states, so only the pathological XSTS particle schemas (the /// msMeta Particles set, which OOM-killed the suite at 8 GB, #129) /// reach it. The ceiling sits well above a single maxed particle /// (~33k states) so capped-but-not-nested schemas are unaffected. private static let totalStateCap = 1 << 20 private var stateBudgetExhausted: Bool { states.count >= Self.totalStateCap } private mutating func particle(_ particle: PureXML.Schema.Particle) -> (start: Int, accept: Int) { let start = addState() var current = start let boundedMin = Swift.min(particle.minOccurs, Self.occursUnrollCap) var unrolledMin = 0 for _ in 0 ..< boundedMin { if stateBudgetExhausted { break } let part = term(particle.term) states[current].epsilon.append(part.start) current = part.accept unrolledMin += 1 } let accept = addState() if stateBudgetExhausted || unrolledMin < boundedMin || particle.minOccurs > Self.occursUnrollCap { // The tail of an absurd minOccurs, or a run that hit the total // state ceiling mid-unroll, is approximated as star. appendStar(particle.term, from: current, to: accept) } else if let maximum = particle.maxOccurs, maximum - boundedMin <= Self.occursUnrollCap { appendOptional(particle.term, count: maximum - boundedMin, from: current, to: accept) } else { appendStar(particle.term, from: current, to: accept) } return (start, accept) } private mutating func appendOptional(_ term: PureXML.Schema.Term, count: Int, from: Int, to accept: Int) { var current = from for _ in 0 ..< Swift.max(0, count) { if stateBudgetExhausted { // Ran out of state budget: let the remainder repeat freely // (star) rather than keep allocating bounded optionals. appendStar(term, from: current, to: accept) return } let part = self.term(term) states[current].epsilon.append(part.start) states[current].epsilon.append(accept) current = part.accept } states[current].epsilon.append(accept) } private mutating func appendStar(_ term: PureXML.Schema.Term, from current: Int, to accept: Int) { let part = self.term(term) states[current].epsilon.append(part.start) states[current].epsilon.append(accept) states[part.accept].epsilon.append(part.start) states[part.accept].epsilon.append(accept) } private mutating func term(_ term: PureXML.Schema.Term) -> (start: Int, accept: Int) { switch term { case let .element(name, _, _): labeled(.name(name)) case let .wildcard(wildcard): labeled(.wildcard(wildcard)) case let .group(group): self.group(group) } } private mutating func labeled(_ label: TermLabel) -> (start: Int, accept: Int) { let start = addState() let accept = addState() states[start].label = label states[start].target = accept return (start, accept) } private mutating func group(_ group: PureXML.Schema.Group) -> (start: Int, accept: Int) { // `all` is validated by counting, not in the NFA; treat it as a // sequence here so a nested occurrence still parses. group.compositor == .choice ? choice(group.particles) : sequence(group.particles) } private mutating func sequence(_ particles: [PureXML.Schema.Particle]) -> (start: Int, accept: Int) { guard !particles.isEmpty else { return labeledEmpty() } var first: Int? var last: Int? for member in particles { let part = particle(member) if let previous = last { states[previous].epsilon.append(part.start) } else { first = part.start } last = part.accept } return (first ?? addState(), last ?? addState()) } private mutating func choice(_ particles: [PureXML.Schema.Particle]) -> (start: Int, accept: Int) { let start = addState() let accept = addState() for member in particles { let part = particle(member) states[start].epsilon.append(part.start) states[part.accept].epsilon.append(accept) } return (start, accept) } private mutating func labeledEmpty() -> (start: Int, accept: Int) { let start = addState() let accept = addState() states[start].epsilon.append(accept) return (start, accept) } } }