PureXML/Sources/Schema/XSDSimpleContentParser.swift
Mihaela Mihaljevic 8acf8ffff6
perf(schema): memoize simpleContent base resolution so a chain is linear, not cubic (#374)
A long <xs:complexType> simpleContent extension chain (T0 extends T1 extends T2 ...) is flat in the document, so each of the N complex types re-walked its whole base chain to resolve the simple type its simpleContent derives from (an O(n^3) compile; a 400-link chain took ~23s). The resolved simple type is now memoized by complex-type name (SimpleContentMemo, a reference type surviving the value-type context copies, mirroring GroupParticleMemo); the same chain compiles in ~1.2s, and past the 512 expansion-depth bound it is rejected with a located error as before. Behaviour-preserving (an acyclic resolution is intrinsic to the name; a derivation cycle is rejected elsewhere), verified conformance-neutral over the full W3C XSTS corpus, the determinism differential, and the full suite. The attribute flattening along the chain is inherently quadratic in the bounded length and is tracked.
2026-06-29 03:26:07 +02:00

76 lines
3.8 KiB
Swift

private typealias SimpleContentNode = PureXML.Schema.XSDNode
private typealias SimpleContentType = PureXML.Schema.SimpleType
private typealias SimpleContentContext = PureXML.Schema.XSDContext
private typealias SimpleContentBuiltinType = PureXML.Schema.BuiltinType
extension PureXML.Schema.XSDParser {
static func simpleContentType(
_ node: XSDTree,
_ context: PureXML.Schema.XSDContext,
visited: Set<String> = [],
) -> PureXML.Schema.SimpleType {
guard let inner = derivation(node) else { return SimpleContentType(base: .string) }
let rawBase = SimpleContentNode.attribute(inner, "base") ?? "string"
let baseName = SimpleContentNode.stripPrefix(rawBase)
let bindings = namespaceBindingsInScope(of: inner, defaultBindings: context.namespaceBindings)
let uri = SimpleContentNode.referenceNamespace(rawBase, bindings)
let baseType = simpleContentBaseType(baseName, uri: uri, context, visited: visited)
let effectiveType: SimpleContentType
let inlineType = SimpleContentNode.localName(inner) == "restriction"
? SimpleContentNode.firstChild(inner, named: "simpleType")
: nil
if let inline = inlineType {
effectiveType = scopedSimpleType(inline, context)
} else {
effectiveType = baseType
}
var facets = effectiveType.facets
let declaresEnumeration = SimpleContentNode.elementChildren(inner)
.contains { SimpleContentNode.localName($0) == "enumeration" }
if SimpleContentNode.localName(inner) == "restriction", declaresEnumeration {
facets.enumeration = nil
}
PureXML.Schema.XSDSimpleParser.applyFacets(inner, into: &facets)
return SimpleContentType(base: effectiveType.base, facets: facets, variety: effectiveType.variety, isBuiltinList: effectiveType.isBuiltinList)
}
static func scopedSimpleType(_ node: XSDTree, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.SimpleType {
var scoped = context
scoped.namespaceBindings = namespaceBindingsInScope(of: node, defaultBindings: context.namespaceBindings)
return PureXML.Schema.XSDSimpleParser.simpleType(node, scoped)
}
/// The simple type a `simpleContent` derivation derives from: an XSD built-in, a
/// named simple type, or, when the base is another complex type with
/// simpleContent, that type's own resolved simple type. A cycle in the base
/// chain falls back to `string`.
private static func simpleContentBaseType(
_ baseName: String,
uri: String?,
_ context: SimpleContentContext,
visited: Set<String>,
) -> SimpleContentType {
if uri == PureXML.Schema.XSDParser.xsdNamespace {
return SimpleContentType(base: SimpleContentBuiltinType(rawValue: baseName) ?? .string)
}
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) }
let resolved = simpleContentType(baseSimpleContent, context, visited: visited.union([baseName]))
context.simpleContentMemo.store[baseName] = resolved
return resolved
}
}