perf(schema): resolve a simple-type restriction chain in linear, not cubic, time (#373)
A long <xs:simpleType> restriction chain is flat in the document, so the same base was resolved repeatedly: the named simple types were resolved by an N-pass fixpoint, and the facet-applicability variety check re-scanned the schema's children to find each base at every link of every type's chain (compiling a 600-link chain took ~4.4s). Named simple types are now resolved once in dependency order (XSDSimpleTypeOrder), and the variety check uses an O(1) name map and memoizes each type's variety (SchemaSimpleTypeVariety); the same chain now compiles in ~30ms. Behaviour-preserving, verified conformance-neutral over the full W3C XSTS corpus, the determinism differential, and the full suite.
This commit is contained in:
parent
3e4ca0cce3
commit
4024801171
@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Compiling a schema with a long `<xs:simpleType>` restriction chain (`S0` restricting `S1` restricting `S2`, and so on) is no longer near-cubic in the chain length, so a deep chain no longer stalls the compiler, a keystroke-time interactive-safety hang on hostile or generated input. Such a chain is flat in the document, so the same base type was resolved over and over: the named simple types were resolved by an N-pass fixpoint that re-resolved every type on every pass, and the facet-applicability variety check re-scanned the schema's children to find each base at every link of every type's chain. Named simple types are now resolved once in dependency order (base before derived), and the variety check resolves a base through a precomputed name map in constant time and memoizes each type's variety, so a chain is walked once rather than per type. A 600-link restriction chain that took about 4.4 seconds to compile now takes about 30 milliseconds. Behaviour-preserving: the resolved types and the emitted findings are identical, verified conformance-neutral over the full W3C XSTS corpus in release (all four buckets unchanged), the libxml2 determinism differential unchanged, and the full suite. The analogous repeated re-scan in attribute-group reference chains and complex-type `simpleContent` chains is milder and remains, tracked.
|
||||
|
||||
- A generated-instance validation differential against libxml2 (`InstanceValidationDifferentialTests`, opt-in via `INSTANCE_DIFF` and `xmllint`) exercises the per-instance content matcher (the graph-structured-stack `ContentRTNMatcher` now in production) over the open input space: for every generated, both-engines-compile schema it validates many generated `<doc>` instances with PureXML and with libxml2 and compares the verdicts. Where the schema-compile determinism check has its own differential, this characterises instance validation, the cardinal rule's instance half, beyond the finite XSTS corpus. It asserts the cardinal rule (PureXML never rejects an instance libxml2 accepts) and reports false negatives for triage. Over 6568 generated validations (1357 both-valid, 5211 both-invalid) across sequences, choices, `all` groups, group references, occurrences, and wildcards it found zero false positives and zero false negatives, independent confirmation that the matcher agrees with libxml2. Test-only; no shipped behaviour change.
|
||||
|
||||
- The graph-structured-stack content matcher's runtime resource use now has a proven, asserted bound: `ContentRTNMatcher` exposes `peakLiveConfigurations` (the largest live-configuration set in any closure) and `gssNodeCount` (the total graph-structured-stack nodes built), and `ContentRTNDifferentialTests` asserts they stay linear in a shared-group chain's depth (measured `gss = K + 2`, peak configurations `= 3K + 3`), where an explicit return stack would reach `O(distinct stacks)`. The bound is checked by count, not by a machine-dependent wall-clock timeout, so a regression to super-linear fails the test rather than only slowing down. Test-and-metadata only; no shipped behaviour change.
|
||||
|
||||
@ -12,10 +12,12 @@ extension PureXML.Schema.XSDParser {
|
||||
static func simpleTypeVarietyFacetFindings(_ schema: XSDTree) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
let bindings = namespaceBindings(schema)
|
||||
let target = PureXML.Schema.XSDNode.attribute(schema, "targetNamespace")
|
||||
let lookup = VarietyLookup(schema: schema, bindings: bindings, target: target)
|
||||
var varietyCache: [String: String?] = [:]
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
forEachSimpleType(schema) { simpleType in
|
||||
guard let restriction = PureXML.Schema.XSDNode.firstChild(simpleType, named: "restriction"),
|
||||
let variety = restrictionBaseVariety(restriction, schema, bindings, target, [])
|
||||
let variety = restrictionBaseVariety(restriction, lookup, [], &varietyCache)
|
||||
else { return }
|
||||
let allowed = variety == "list" ? listFacets : unionFacets
|
||||
let used = PureXML.Schema.XSDNode.elementChildren(restriction)
|
||||
@ -220,7 +222,7 @@ extension PureXML.Schema.XSDParser {
|
||||
]
|
||||
private static let listFacets: Set<String> = ["length", "minLength", "maxLength", "pattern", "enumeration", "whiteSpace"]
|
||||
private static let unionFacets: Set<String> = ["pattern", "enumeration"]
|
||||
private static let builtinListTypes: Set<String> = ["IDREFS", "ENTITIES", "NMTOKENS"]
|
||||
static let builtinListTypes: Set<String> = ["IDREFS", "ENTITIES", "NMTOKENS"]
|
||||
|
||||
private static func forEachSimpleType(_ node: XSDTree, _ visit: (XSDTree) -> Void) {
|
||||
let local = PureXML.Schema.XSDNode.localName(node)
|
||||
@ -231,51 +233,11 @@ extension PureXML.Schema.XSDParser {
|
||||
}
|
||||
}
|
||||
|
||||
/// The variety (`"list"`, `"union"`, or nil for atomic/unknown) a `restriction`
|
||||
/// derives from: an inline `simpleType` base wins, otherwise the `base` QName.
|
||||
private static func restrictionBaseVariety(_ restriction: XSDTree, _ schema: XSDTree, _ bindings: [String: String], _ target: String?, _ visited: Set<String>) -> String? {
|
||||
if let inlineBase = PureXML.Schema.XSDNode.firstChild(restriction, named: "simpleType") {
|
||||
return simpleTypeVariety(inlineBase, schema, bindings, target, visited)
|
||||
}
|
||||
guard let base = PureXML.Schema.XSDNode.attribute(restriction, "base") else { return nil }
|
||||
return namedTypeVariety(base, schema, bindings, target, visited)
|
||||
}
|
||||
|
||||
private static func simpleTypeVariety(_ simpleType: XSDTree, _ schema: XSDTree, _ bindings: [String: String], _ target: String?, _ visited: Set<String>) -> String? {
|
||||
if PureXML.Schema.XSDNode.firstChild(simpleType, named: "list") != nil { return "list" }
|
||||
if PureXML.Schema.XSDNode.firstChild(simpleType, named: "union") != nil { return "union" }
|
||||
if let restriction = PureXML.Schema.XSDNode.firstChild(simpleType, named: "restriction") {
|
||||
return restrictionBaseVariety(restriction, schema, bindings, target, visited)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// The variety of a named base, resolved namespace-aware so a same-local-name
|
||||
/// collision never flips the variety: a base in the XSD namespace is a built-in
|
||||
/// (only the list datatypes are list-variety; the rest atomic, nil); a base in
|
||||
/// the schema's own target namespace resolves to a declared global `simpleType`
|
||||
/// (cycle-guarded); a base in any other namespace is external and skipped (nil).
|
||||
private static func namedTypeVariety(_ base: String, _ schema: XSDTree, _ bindings: [String: String], _ target: String?, _ visited: Set<String>) -> String? {
|
||||
let (prefix, local) = splitQName(base)
|
||||
let uri = prefix.map { bindings[$0] } ?? bindings[""]
|
||||
if uri == xsdNamespace {
|
||||
return builtinListTypes.contains(local) ? "list" : nil
|
||||
}
|
||||
guard uri == target, !visited.contains(local), let declared = globalSimpleType(named: local, schema) else { return nil }
|
||||
return simpleTypeVariety(declared, schema, bindings, target, visited.union([local]))
|
||||
}
|
||||
|
||||
private static func splitQName(_ value: String) -> (prefix: String?, local: String) {
|
||||
static func splitQName(_ value: String) -> (prefix: String?, local: String) {
|
||||
let parts = value.split(separator: ":", maxSplits: 1).map(String.init)
|
||||
return parts.count == 2 ? (parts[0], parts[1]) : (nil, value)
|
||||
}
|
||||
|
||||
private static func globalSimpleType(named name: String, _ schema: XSDTree) -> XSDTree? {
|
||||
PureXML.Schema.XSDNode.elementChildren(schema).first {
|
||||
PureXML.Schema.XSDNode.localName($0) == "simpleType" && PureXML.Schema.XSDNode.attribute($0, "name") == name
|
||||
}
|
||||
}
|
||||
|
||||
/// The prefix-to-namespace bindings on the schema element (its `xmlns:prefix`
|
||||
/// attributes; the default namespace under the empty key).
|
||||
private static func namespaceBindings(_ schema: XSDTree) -> [String: String] {
|
||||
|
||||
61
Sources/Schema/SchemaSimpleTypeVariety.swift
Normal file
61
Sources/Schema/SchemaSimpleTypeVariety.swift
Normal file
@ -0,0 +1,61 @@
|
||||
extension PureXML.Schema.XSDParser {
|
||||
/// The lookups a simple-type variety walk needs, bundled so the walk's base resolution
|
||||
/// is O(1) (the name map) and threadable without a long parameter list.
|
||||
struct VarietyLookup {
|
||||
let byName: [String: XSDTree]
|
||||
let bindings: [String: String]
|
||||
let target: String?
|
||||
|
||||
/// Built from a schema element: its global `simpleType` definitions keyed by name
|
||||
/// (first declaration wins, matching the prior first-match scan), so the variety
|
||||
/// walk looks a base up in O(1) instead of re-scanning the schema's children at
|
||||
/// every link of a `restriction` chain (the former O(n^3) on a long, hostile chain).
|
||||
init(schema: XSDTree, bindings: [String: String], target: String?) {
|
||||
var byName: [String: XSDTree] = [:]
|
||||
for child in PureXML.Schema.XSDNode.elementChildren(schema) where PureXML.Schema.XSDNode.localName(child) == "simpleType" {
|
||||
if let name = PureXML.Schema.XSDNode.attribute(child, "name"), byName[name] == nil { byName[name] = child }
|
||||
}
|
||||
self.byName = byName
|
||||
self.bindings = bindings
|
||||
self.target = target
|
||||
}
|
||||
}
|
||||
|
||||
/// The variety (`"list"`, `"union"`, or nil for atomic/unknown) a `restriction`
|
||||
/// derives from: an inline `simpleType` base wins, otherwise the `base` QName.
|
||||
/// `cache` memoizes each named type's variety, so a chain is walked once, not per type.
|
||||
static func restrictionBaseVariety(_ restriction: XSDTree, _ lookup: VarietyLookup, _ visited: Set<String>, _ cache: inout [String: String?]) -> String? {
|
||||
if let inlineBase = PureXML.Schema.XSDNode.firstChild(restriction, named: "simpleType") {
|
||||
return simpleTypeVariety(inlineBase, lookup, visited, &cache)
|
||||
}
|
||||
guard let base = PureXML.Schema.XSDNode.attribute(restriction, "base") else { return nil }
|
||||
return namedTypeVariety(base, lookup, visited, &cache)
|
||||
}
|
||||
|
||||
private static func simpleTypeVariety(_ simpleType: XSDTree, _ lookup: VarietyLookup, _ visited: Set<String>, _ cache: inout [String: String?]) -> String? {
|
||||
if PureXML.Schema.XSDNode.firstChild(simpleType, named: "list") != nil { return "list" }
|
||||
if PureXML.Schema.XSDNode.firstChild(simpleType, named: "union") != nil { return "union" }
|
||||
if let restriction = PureXML.Schema.XSDNode.firstChild(simpleType, named: "restriction") {
|
||||
return restrictionBaseVariety(restriction, lookup, visited, &cache)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// The variety of a named base, resolved namespace-aware so a same-local-name
|
||||
/// collision never flips the variety: a base in the XSD namespace is a built-in
|
||||
/// (only the list datatypes are list-variety; the rest atomic, nil); a base in
|
||||
/// the schema's own target namespace resolves to a declared global `simpleType`
|
||||
/// (cycle-guarded); a base in any other namespace is external and skipped (nil).
|
||||
private static func namedTypeVariety(_ base: String, _ lookup: VarietyLookup, _ visited: Set<String>, _ cache: inout [String: String?]) -> String? {
|
||||
let (prefix, local) = splitQName(base)
|
||||
let uri = prefix.map { lookup.bindings[$0] } ?? lookup.bindings[""]
|
||||
if uri == xsdNamespace {
|
||||
return builtinListTypes.contains(local) ? "list" : nil
|
||||
}
|
||||
if let cached = cache[local] { return cached }
|
||||
guard uri == lookup.target, !visited.contains(local), let declared = lookup.byName[local] else { return nil }
|
||||
let variety = simpleTypeVariety(declared, lookup, visited.union([local]), &cache)
|
||||
cache[local] = variety
|
||||
return variety
|
||||
}
|
||||
}
|
||||
@ -357,12 +357,12 @@ extension PureXML.Schema.XSDParser {
|
||||
/// dependency chain of length n settles in at most n passes.
|
||||
private static func resolveSimpleTypes(_ nodes: [XSDTree], into context: inout XSDContext) {
|
||||
let named = nodes.filter { XSDNode.attribute($0, "name") != nil }
|
||||
for _ in named.indices {
|
||||
for node in named {
|
||||
if let name = XSDNode.attribute(node, "name") {
|
||||
context.simpleTypes[name] = XSDSimpleParser.simpleType(node, context)
|
||||
}
|
||||
}
|
||||
let nodeByName = simpleTypeNodesByName(named)
|
||||
// Resolve in dependency order (base before derived) so each type is compiled once,
|
||||
// not re-resolved N times by the former fixpoint; the resolved set is the same.
|
||||
for name in simpleTypeResolutionOrder(named, nodeByName: nodeByName) {
|
||||
guard let node = nodeByName[name] else { continue }
|
||||
context.simpleTypes[name] = XSDSimpleParser.simpleType(node, context)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
67
Sources/Schema/XSDSimpleTypeOrder.swift
Normal file
67
Sources/Schema/XSDSimpleTypeOrder.swift
Normal file
@ -0,0 +1,67 @@
|
||||
private typealias XSDNode = PureXML.Schema.XSDNode
|
||||
|
||||
extension PureXML.Schema.XSDParser {
|
||||
/// The named `<xs:simpleType>` nodes keyed by name (first declaration wins, matching
|
||||
/// the resolution loop, which overwrites a later same-name node's slot last).
|
||||
static func simpleTypeNodesByName(_ named: [PureXML.Model.TreeNode]) -> [String: PureXML.Model.TreeNode] {
|
||||
var byName: [String: PureXML.Model.TreeNode] = [:]
|
||||
for node in named {
|
||||
if let name = XSDNode.attribute(node, "name"), byName[name] == nil { byName[name] = node }
|
||||
}
|
||||
return byName
|
||||
}
|
||||
|
||||
/// The named simple types in dependency order: each type after every named type it
|
||||
/// derives from (its `restriction` base, `list` item, or `union` members, including
|
||||
/// those reached through an inline anonymous base). Resolving in this order lets each
|
||||
/// type be compiled ONCE with its base already in `context.simpleTypes`, where the
|
||||
/// former fixpoint re-resolved every type N times (super-linear on a long restriction
|
||||
/// chain). An iterative post-order DFS, so it does not recurse on the chain depth; a
|
||||
/// name already on the current path is not re-entered, so a circular reference
|
||||
/// (illegal, rejected elsewhere) terminates and its types still get an order.
|
||||
static func simpleTypeResolutionOrder(
|
||||
_ named: [PureXML.Model.TreeNode],
|
||||
nodeByName: [String: PureXML.Model.TreeNode],
|
||||
) -> [String] {
|
||||
var order: [String] = []
|
||||
var visited: Set<String> = []
|
||||
var onPath: Set<String> = []
|
||||
var work: [(name: String, exit: Bool)] = named
|
||||
.compactMap { XSDNode.attribute($0, "name") }
|
||||
.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))
|
||||
guard let node = nodeByName[name] else { continue }
|
||||
for dependency in referencedSimpleTypeNames(in: node) {
|
||||
guard nodeByName[dependency] != nil, !visited.contains(dependency), !onPath.contains(dependency) else { continue }
|
||||
work.append((dependency, false))
|
||||
}
|
||||
}
|
||||
return order
|
||||
}
|
||||
|
||||
/// The type names a `<xs:simpleType>` node references through `base`, `itemType`, or
|
||||
/// `memberTypes` (the last a space-separated list), anywhere in its subtree, by an
|
||||
/// iterative walk. The caller keeps only those that name another type in the set.
|
||||
private static func referencedSimpleTypeNames(in node: PureXML.Model.TreeNode) -> [String] {
|
||||
var names: [String] = []
|
||||
var stack = [node]
|
||||
while let current = stack.popLast() {
|
||||
for attribute in ["base", "itemType", "memberTypes"] {
|
||||
guard let value = XSDNode.attribute(current, attribute) else { continue }
|
||||
names += value.split(separator: " ").map { XSDNode.stripPrefix(String($0)) }
|
||||
}
|
||||
stack.append(contentsOf: XSDNode.elementChildren(current))
|
||||
}
|
||||
return names
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user