@testable import PureXML /// An independent, brute-force content-model membership oracle over element names, /// deliberately implemented separately from PureXML's matcher so the instance /// differential can adjudicate a disagreement with libxml2: libxml2's content /// validation has leniency bugs (it accepts some invalid nested/nullable/unbounded /// models), so "PureXML rejects, libxml2 accepts" is not by itself a PureXML fault. /// The generated instances are empty `xs:string` elements, so an instance is valid /// iff its element-name sequence is in this language; this oracle is that decision, /// verified against an external regex engine on the known cases. indirect enum ContentModel: Sendable { case element(String) case any case sequence([ContentModel]) case choice([ContentModel]) /// An `xs:all` group; each member is its own `occurs(element, min, 1)`. case all([ContentModel]) case occurs(ContentModel, Int, Int?) } enum ContentModelOracle { /// Whether the model can match the empty sequence. static func nullable(_ model: ContentModel) -> Bool { switch model { case .element, .any: false case let .sequence(parts): parts.allSatisfy(nullable) case let .choice(parts): parts.contains(where: nullable) case let .all(parts): parts.allSatisfy(nullable) case let .occurs(sub, low, _): low == 0 || nullable(sub) } } /// Every position reachable after matching `model` starting at `start`. static func ends(_ model: ContentModel, _ tokens: [String], _ start: Int) -> Set { switch model { case let .element(name): return start < tokens.count && tokens[start] == name ? [start + 1] : [] case .any: return start < tokens.count ? [start + 1] : [] case let .sequence(parts): var positions: Set = [start] for part in parts { var next: Set = [] for position in positions { next.formUnion(ends(part, tokens, position)) } positions = next if positions.isEmpty { break } } return positions case let .choice(parts): var out: Set = [] for part in parts { out.formUnion(ends(part, tokens, start)) } return out case let .all(parts): return allEnds(parts, Array(repeating: false, count: parts.count), tokens, start) case let .occurs(sub, low, high): return occursEnds(sub, low, high, tokens, start) } } /// Positions reachable by matching `sub` between `low` and `high` times. A /// nullable `sub` is bounded by stopping once the reachable set stops growing. private static func occursEnds(_ sub: ContentModel, _ low: Int, _ high: Int?, _ tokens: [String], _ start: Int) -> Set { var byReps: [Set] = [[start]] let cap = min(high ?? Int.max, tokens.count + low + 2) var reps = 0 while reps < cap { var next: Set = [] for position in byReps[reps] { next.formUnion(ends(sub, tokens, position)) } byReps.append(next) reps += 1 let priorPositions = byReps[0 ..< reps].reduce(into: Set()) { $0.formUnion($1) } if next.isSubset(of: priorPositions), reps >= low { break } } var out: Set = [] if low == 0 { out.insert(start) } for count in low ... cap where count < byReps.count { out.formUnion(byReps[count]) } return out } /// Positions reachable by matching the remaining (not-`done`) members of an /// `xs:all` group in any order; a member that is nullable may be skipped. private static func allEnds(_ members: [ContentModel], _ done: [Bool], _ tokens: [String], _ start: Int) -> Set { var out: Set = [] if members.indices.allSatisfy({ done[$0] || nullable(members[$0]) }) { out.insert(start) } for index in members.indices where !done[index] { for end in ends(members[index], tokens, start) where end > start { var next = done next[index] = true out.formUnion(allEnds(members, next, tokens, end)) } } return out } static func matches(_ model: ContentModel, _ tokens: [String]) -> Bool { ends(model, tokens, 0).contains(tokens.count) } }