/// Orders the model groups reachable from a set of content models so each group /// comes after every group it references. The compositional determinism check /// summarizes groups in this order, so its recursive walk is always a cache hit at a /// `` and never recurses through a reference chain. A chain is FLAT in the /// schema document (group definitions are siblings, not nested), so it is not bounded /// by the parser's `maxDepth`; ordering and scanning here are iterative so neither /// step recurses on the chain length either. enum GroupDependencyOrder { /// Reachable group names in dependency order (dependencies first), by an iterative /// post-order DFS from the groups the `models` reference. A name already on the /// current path is not re-entered, so a circular reference (illegal, rejected /// elsewhere) terminates. static func reachable(from models: [PureXML.Model.TreeNode], context: PureXML.Schema.XSDContext) -> [String] { var roots: [String] = [] var seen: Set = [] for model in models { for ref in referencedGroupNames(in: model) where seen.insert(ref).inserted { roots.append(ref) } } var order: [String] = [] var visited: Set = [] var onPath: Set = [] var work: [(name: String, exit: Bool)] = roots.reversed().map { ($0, false) } while let (name, exit) = work.popLast() { if exit { onPath.remove(name) order.append(name) continue } if visited.contains(name) { continue } visited.insert(name) onPath.insert(name) work.append((name, true)) for ref in dependencies(of: name, context: context).reversed() where !visited.contains(ref) && !onPath.contains(ref) { work.append((ref, false)) } } return order } /// The group names referenced within `node`, by an iterative tree walk (so scanning /// a deep model does not itself recurse on depth). It descends compositors but stops /// at an ``, exactly as the determinism walk's `summarize` treats an element /// as terminal: a group inside an element's inline complex type belongs to THAT type's /// own content model (checked separately), not this one, and must not be drawn in. private static func referencedGroupNames(in node: PureXML.Model.TreeNode) -> [String] { var names: [String] = [] var stack = [node] while let current = stack.popLast() { let local = PureXML.Schema.XSDNode.localName(current) if local == "group", let ref = PureXML.Schema.XSDNode.attribute(current, "ref") { names.append(PureXML.Schema.XSDNode.stripPrefix(ref)) } if local != "element" { stack.append(contentsOf: PureXML.Schema.XSDNode.elementChildren(current)) } } return names } /// The groups `name` references. A redefined group's base-model references are added /// ONLY when the redefinition actually self-references (`` inside the /// redefinition of `name`): that reference expands to the base model /// (`CompWalk.redefineSelfReference`), making the base's groups part of the effective /// content model, so they must be ordered first. When the redefinition does NOT /// self-reference, the base is fully replaced and dead, and pulling its refs in would /// summarise unreachable groups: surfacing a conflict in a dead group is a false /// positive, so it is deliberately excluded. This keeps the reachable set identical /// to the lazy walk's. private static func dependencies(of name: String, context: PureXML.Schema.XSDContext) -> [String] { guard let definition = context.groups[name], let model = modelChild(definition) else { return [] } var refs = referencedGroupNames(in: model) if refs.contains(name), context.redefinedGroups.contains(name), let base = context.baseGroups[name], let baseModel = modelChild(base) { refs += referencedGroupNames(in: baseModel) } return refs } /// The `sequence`/`choice`/`all` model child of a group definition, if any. private static func modelChild(_ definition: PureXML.Model.TreeNode) -> PureXML.Model.TreeNode? { PureXML.Schema.XSDNode.elementChildren(definition) .first { ["sequence", "choice", "all"].contains(PureXML.Schema.XSDNode.localName($0) ?? "") } } }