PureXML/Sources/Schema/ContentModelDeterminismConflict.swift
Mihaela Mihaljevic 1f8d99adfc
perf(schema): make the UPA determinism check depth-safe on group-reference chains (#371)
A flat chain of <xs:group> references is not bounded by the parser's maxDepth (the definitions are siblings, not nested), so the compositional UPA determinism check's lazy per-reference recursion overflowed the native 8 MB stack while compiling a legal schema. Group summaries form an acyclic graph, so they are now precomputed bottom-up in dependency order (GroupDependencyOrder): the walk's group lookup is a cache hit and recursion is bounded by a single group's own model nesting. The precomputed reachable set is identical to the lazy walk's, so the verdict is unchanged. Verified verdict-equivalent over the full W3C XSTS corpus, the libxml2 determinism differential (0 FP, same 3 FN), the full suite, and an adversarial critic. Residual, tracked: the particle compiler that inlines a group reference still overflows on a far longer chain; closing it needs a content-model nesting-depth limit with a located error.
2026-06-28 23:15:32 +02:00

48 lines
2.6 KiB
Swift

/// The overlap tests for a compositional-determinism decision set: a UPA violation
/// is two DISTINCT particles in one decision set that can match the same element
/// (same element name, or a wildcard admitting the other's name/namespace). Pure
/// functions over ``CompItem`` sets, factored out of ``CompWalk`` so the walk file
/// stays within its length budget; the walk accumulates decision sets and hands
/// each to ``overlap(_:)``.
enum CompConflict {
static func overlap(_ set: [CompItem]) -> String? {
var nameOwner: [PureXML.Model.QualifiedName: ObjectIdentifier] = [:]
var wildcards: [(card: PureXML.Schema.Wildcard, particle: ObjectIdentifier)] = []
for item in set {
switch item.label {
case let .name(qname):
if let owner = nameOwner[qname], owner != item.particle { return qname.localName }
if nameOwner[qname] == nil { nameOwner[qname] = item.particle }
case let .wildcard(card):
wildcards.append((card, item.particle))
}
}
for (index, wildcard) in wildcards.enumerated() {
// A wildcard conflicts with any name it admits, from a distinct particle,
// and with any other wildcard it intersects, from a distinct particle.
if nameOwner.contains(where: { wildcard.card.admits($0.key) && $0.value != wildcard.particle }) { return "any" }
for other in wildcards[(index + 1)...] where other.particle != wildcard.particle && wildcardsOverlap(wildcard.card, other.card) {
return "any"
}
}
return nil
}
static func wildcardsOverlap(_ lhs: PureXML.Schema.Wildcard, _ rhs: PureXML.Schema.Wildcard) -> Bool {
// Two wildcards overlap when some namespace name is admitted by both. Any
// two `not` forms always share infinitely many named namespaces; a `not`
// form overlaps a set when the set holds a named namespace it admits.
switch (lhs.namespace, rhs.namespace) {
case (.any, _), (_, .any): true
case let (.enumerated(lhsSet), .enumerated(rhsSet)): !lhsSet.isDisjoint(with: rhsSet)
case let (.enumerated(set), .notNamespace(name)), let (.notNamespace(name), .enumerated(set)):
set.contains { !$0.isEmpty && $0 != name }
case let (.enumerated(set), .notAbsent), let (.notAbsent, .enumerated(set)):
set.contains { !$0.isEmpty }
case (.notNamespace, .notNamespace), (.notNamespace, .notAbsent),
(.notAbsent, .notNamespace), (.notAbsent, .notAbsent):
true
}
}
}