extension PureXML.Schema { /// A call edge: enter the callee sub-program at `calleeStart`, returning to /// `returnTo` when the callee's return state is reached. The `actions` reset the /// callee's counter scopes on entry (so a shared fragment entered twice does not /// carry a stale count); `guards` are the calling occurrence's `belowMaximum`. struct ContentCallEdge: Sendable { var calleeStart: Int var returnTo: Int var guards: [ContentCounterGuard] = [] var actions: [ContentCounterAction] = [] } /// One state of the counted Recursive Transition Network: like ``ContentState`` /// but a state may also `call` a shared group fragment, and a group fragment's /// accept is a `return` (it pops the graph-structured stack to its caller). struct ContentRTNState: Sendable { var epsilon: [ContentEpsilonEdge] = [] var calls: [ContentCallEdge] = [] /// A group fragment's accept: pop the GSS and continue at the caller's /// `returnTo`. The top-level program accept is NOT a return. var isReturn: Bool = false var label: TermLabel? var target: Int? var elementType: ElementType? var valueConstraint: ValueConstraint? } /// A counted RTN compiled from a ``Particle``: each `` definition /// (identified by ``Group/definitionID``) is compiled once and reached by a /// `call`, so a multiply-referenced group is shared, not inlined `2^K` times. /// `all` groups are validated separately and are not represented here, exactly /// as in ``ContentNFA``. struct ContentRTN: Sendable { var states: [ContentRTNState] var counters: [ContentCounterScope] var start: Int var accept: Int } } extension PureXML.Schema { /// Builds a counted ``ContentRTN`` from a particle, sharing each `definitionID` /// group fragment. The occurrence-wrapper shape (reset/loop/exit/increment) /// matches ``ContentNFABuilder`` exactly; the only difference is that a group /// with a `definitionID` compiles to a `call` into its shared fragment instead /// of being inlined. struct ContentRTNBuilder { private var states: [ContentRTNState] = [] private var counters: [ContentCounterScope] = [] /// The compiled (start, accept, counters) of each shared group fragment. private var fragments: [Int: ContentNFAFragment] = [:] /// Definition ids whose fragment is mid-compilation, to detect a (malformed) /// cyclic group definition instead of recurring without bound. private var compiling: Set = [] static func build(_ particle: PureXML.Schema.Particle) -> ContentRTN { var builder = ContentRTNBuilder() let fragment = builder.particle(particle) return ContentRTN( states: builder.states, counters: builder.counters, start: fragment.start, accept: fragment.accept, ) } private mutating func addState() -> Int { states.append(ContentRTNState()) return states.count - 1 } private mutating func addEpsilon( from source: Int, to target: Int, guards: [ContentCounterGuard] = [], actions: [ContentCounterAction] = [], ) { states[source].epsilon.append(ContentEpsilonEdge(target: target, guards: guards, actions: actions)) } private mutating func particle(_ particle: PureXML.Schema.Particle) -> ContentNFAFragment { let start = addState() let loop = addState() let accept = addState() let counter = counters.count let bodyNullable = termIsNullable(particle.term) counters.append(ContentCounterScope(range: particle.occurrenceRange, nullableBody: bodyNullable)) let body = term(particle.term) let resetAll = [ContentCounterAction.reset(counter)] + body.counters.map(ContentCounterAction.reset) addEpsilon(from: start, to: loop, actions: resetAll) addEpsilon( from: loop, to: accept, guards: [.canExit(counter: counter, minimum: particle.occurrenceRange.minimum, nullableBody: bodyNullable)], ) addEpsilon( from: loop, to: body.start, guards: [.belowMaximum(counter: counter, maximum: particle.occurrenceRange.maximum)], actions: body.counters.map(ContentCounterAction.reset), ) addEpsilon(from: body.accept, to: loop, actions: [.increment(counter)]) return ContentNFAFragment(start: start, accept: accept, counters: [counter] + body.counters, nullable: particleIsNullable(particle)) } private mutating func term(_ term: PureXML.Schema.Term) -> ContentNFAFragment { switch term { case let .element(name, type, _, constraint, _, _): let fragment = labeled(.name(name)) states[fragment.start].elementType = type states[fragment.start].valueConstraint = constraint return fragment case let .wildcard(wildcard): return labeled(.wildcard(wildcard)) case let .group(group): if let id = group.definitionID { return call(group, id: id) } return self.group(group) } } /// A reference to a shared group: a `call` state into the group's fragment /// (compiled once), returning to a fresh accept. The callee's counters are /// deliberately NOT propagated to the caller: every counter scope is reset on /// its own particle's entry edge, which is on the path from the callee's start, /// so the shared fragment resets itself on every (re-)entry without the caller /// re-listing it. Propagating them would re-list each shared fragment's counters /// at every reference, nesting to a `2^K` reset-action list even though the /// states stay `O(K)`, which is the whole point of sharing. private mutating func call(_ group: PureXML.Schema.Group, id: Int) -> ContentNFAFragment { let fragment = groupFragment(group, id: id) let callState = addState() let returnState = addState() states[callState].calls.append(ContentCallEdge(calleeStart: fragment.start, returnTo: returnState)) return ContentNFAFragment(start: callState, accept: returnState, counters: [], nullable: fragment.nullable) } /// The shared fragment for a group definition, compiled once and memoized. /// Its accept is marked `isReturn`. private mutating func groupFragment(_ group: PureXML.Schema.Group, id: Int) -> ContentNFAFragment { if let cached = fragments[id] { return cached } // `definitionID` groups are acyclic: XSD forbids a group referencing itself // except through an element, and the parser's expansion breaks every cycle at // an element boundary, so a fragment is never referenced during its own // compilation (the inlining `ContentNFABuilder` relies on the same property). // Guard it anyway: a cyclic id would otherwise recur without bound, the exact // interactive-safety hazard this matcher exists to remove, so bound the // recursion by degrading a (malformed, unreachable) cycle to a terminating // empty fragment rather than overflowing the stack. guard compiling.insert(id).inserted else { return labeledEmpty() } defer { compiling.remove(id) } let built = group.compositor == .choice ? choice(group.particles) : sequence(group.particles) states[built.accept].isReturn = true fragments[id] = built return built } private mutating func group(_ group: PureXML.Schema.Group) -> ContentNFAFragment { group.compositor == .choice ? choice(group.particles) : sequence(group.particles) } private mutating func labeled(_ label: TermLabel) -> ContentNFAFragment { let start = addState() let accept = addState() states[start].label = label states[start].target = accept return ContentNFAFragment(start: start, accept: accept, counters: [], nullable: false) } private mutating func sequence(_ particles: [PureXML.Schema.Particle]) -> ContentNFAFragment { guard !particles.isEmpty else { return labeledEmpty() } var first: Int? var last: Int? var fragmentCounters: [Int] = [] var nullable = true for member in particles { let part = particle(member) if let previous = last { addEpsilon(from: previous, to: part.start) } else { first = part.start } last = part.accept fragmentCounters.append(contentsOf: part.counters) nullable = nullable && part.nullable } return ContentNFAFragment(start: first ?? addState(), accept: last ?? addState(), counters: fragmentCounters, nullable: nullable) } private mutating func choice(_ particles: [PureXML.Schema.Particle]) -> ContentNFAFragment { let start = addState() let accept = addState() var fragmentCounters: [Int] = [] var nullable = false for member in particles { let part = particle(member) addEpsilon(from: start, to: part.start) addEpsilon(from: part.accept, to: accept) fragmentCounters.append(contentsOf: part.counters) nullable = nullable || part.nullable } return ContentNFAFragment(start: start, accept: accept, counters: fragmentCounters, nullable: nullable) } private mutating func labeledEmpty() -> ContentNFAFragment { let start = addState() let accept = addState() addEpsilon(from: start, to: accept) return ContentNFAFragment(start: start, accept: accept, counters: [], nullable: true) } private func particleIsNullable(_ particle: PureXML.Schema.Particle) -> Bool { particle.occurrenceRange.minimum.isZero || termIsNullable(particle.term) } private func termIsNullable(_ term: PureXML.Schema.Term) -> Bool { switch term { case .element, .wildcard: false case let .group(group): switch group.compositor { case .sequence, .all: group.particles.allSatisfy(particleIsNullable) case .choice: group.particles.contains(where: particleIsNullable) } } } } }