diff --git a/CHANGELOG.md b/CHANGELOG.md index d32a926..78a45c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Compiling a schema with a long `` 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. +- Compiling a schema with a long `` 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 is milder and remains, tracked. + +- Compiling a schema with a long `` `simpleContent` extension chain (`T0` extending `T1` extending `T2`, and so on) is likewise no longer near-cubic. Each of the N types re-walked its whole base chain to resolve the simple type its `simpleContent` derives from, the same flat-document re-resolution. The resolved simple type of a complex type's `simpleContent` is now memoized by name (`SimpleContentMemo`, a reference type that survives the value-type context copies), so a chain is walked once rather than per type. A 400-link chain that took about 23 seconds to compile now takes about 1.2 seconds; past the content-model expansion-depth bound (512) such a chain is rejected with a located error as before. Behaviour-preserving: the resolved type is intrinsic to the name for an acyclic chain (a derivation cycle is rejected elsewhere), verified conformance-neutral over the full W3C XSTS corpus in release. The attribute flattening that accumulates each type's attributes along the chain is inherently quadratic in the bounded chain length and is 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 `` 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. diff --git a/Sources/Schema/SimpleContentMemo.swift b/Sources/Schema/SimpleContentMemo.swift new file mode 100644 index 0000000..c688777 --- /dev/null +++ b/Sources/Schema/SimpleContentMemo.swift @@ -0,0 +1,13 @@ +extension PureXML.Schema { + /// Memoizes the resolved simple type of a complex type's `simpleContent`, keyed by the + /// complex type's name, during one compile. A `simpleContent` derivation chain (`T0` + /// extends `T1` extends `T2`, and so on) is flat in the document, so without this each + /// of the N types re-walks its whole base chain while being compiled, an O(n^3) compile + /// (a 400-link chain took ~23 s); sharing the resolution makes it linear. A reference + /// type so it survives the value-type ``XSDContext`` copies (`scoped`/`visiting`); one + /// instance per compile. An acyclic resolution is intrinsic to the name (a derivation + /// cycle is rejected elsewhere), so the name is a sufficient key. + final class SimpleContentMemo { + var store: [String: PureXML.Schema.SimpleType] = [:] + } +} diff --git a/Sources/Schema/XSDContext.swift b/Sources/Schema/XSDContext.swift index 0a4b1db..4246a3d 100644 --- a/Sources/Schema/XSDContext.swift +++ b/Sources/Schema/XSDContext.swift @@ -289,6 +289,7 @@ extension PureXML.Schema { struct XSDContext { var simpleTypes: [String: SimpleType] let groupMemo = GroupParticleMemo() + let simpleContentMemo = SimpleContentMemo() var attributeGroups: [String: XSDTree] /// Attribute-group definitions from included schemas before a `redefine` /// overlay replaces them; used when a redefinition self-references its @@ -340,8 +341,7 @@ extension PureXML.Schema { /// reference to an abstract head expands to its members but not the head. var abstractElements: Set = [] var visitingGroups: Set = [] - /// Particle-compiler expansion depth (each compositor level and `` - /// expansion adds one); `groupParticle` bounds it (`maximumContentModelExpansionDepth`). + /// Particle-compiler expansion depth, bounded by `maximumContentModelExpansionDepth`. var expansionDepth = 0 /// Named complex types being resolved up the current `complexContent` /// derivation chain, a guard against a cyclic base reference. diff --git a/Sources/Schema/XSDSimpleContentParser.swift b/Sources/Schema/XSDSimpleContentParser.swift index 0c6140f..bf9ae01 100644 --- a/Sources/Schema/XSDSimpleContentParser.swift +++ b/Sources/Schema/XSDSimpleContentParser.swift @@ -57,10 +57,19 @@ extension PureXML.Schema.XSDParser { if let simple = context.simpleTypes[baseName] { return simple } + // A complex-type `simpleContent` base resolves to that type's own simple type. + // Memoize it by name so a long extension chain is walked once, not re-walked while + // compiling every type on it (the former O(n^3)); the resolution is intrinsic to the + // name for an acyclic chain, and a derivation cycle is rejected elsewhere. + if let memoized = context.simpleContentMemo.store[baseName] { + return memoized + } guard !visited.contains(baseName), let complexNode = context.complexTypeNodes[baseName], let baseSimpleContent = SimpleContentNode.firstChild(complexNode, named: "simpleContent") else { return SimpleContentType(base: .string) } - return simpleContentType(baseSimpleContent, context, visited: visited.union([baseName])) + let resolved = simpleContentType(baseSimpleContent, context, visited: visited.union([baseName])) + context.simpleContentMemo.store[baseName] = resolved + return resolved } }