PureXML/Sources/Model/TreeNodeConversion.swift
Mihaela Mihaljevic cc2e1aaf7c
perf(parse): adopt prepared children when materializing the tree (#293)
Building the mutable TreeNode tree from a parsed value Node ran append for every
child. append -> insert detaches the child from any previous parent, rejects a
self/ancestor cycle, and inserts into a children array that grows element by
element. The children the conversion passes come from element.children.map and
are freshly built and parentless, so all three guards are no-ops and the
incremental growth rebuilds an array that was already prepared.

TreeNode.init(adopting:) adopts the prepared array directly and wires each
child's parent. The tree is identical: the same children in the same order with
the same parent links. The adopting initializer is used only by the value-Node
conversion; every public factory keeps the detaching, cycle-checking append path
for externally supplied children that may already live in a tree.

Measured parse ~12% faster on the 20k-item generated corpus via a same-state
A/B (7.7x to ~6.5x slower than libxml2). Verified by TreeNodeAdoptingInitTests
(parent-wiring and post-adoption mutability), the W3C XML conformance corpora,
the Xalan XSLT gold-output corpus, the full suite, the WASM build, and an
adversarial equivalence review.
2026-06-25 13:02:20 +02:00

71 lines
2.7 KiB
Swift

public extension PureXML.Model.TreeNode {
/// Builds a mutable tree from a parsed value ``Node``, wiring up parent links.
convenience init(_ node: PureXML.Model.Node) {
switch node {
case let .document(children):
self.init(adopting: .document, children: children.map(PureXML.Model.TreeNode.init))
case let .element(element):
self.init(
adopting: .element,
name: element.name,
attributes: element.attributes,
children: element.children.map(PureXML.Model.TreeNode.init),
)
case let .text(value):
self.init(kind: .text, value: value)
case let .cdata(value):
self.init(kind: .cdata, value: value)
case let .comment(value):
self.init(kind: .comment, value: value)
case let .processingInstruction(target, data):
self.init(kind: .processingInstruction, name: PureXML.Model.QualifiedName(target), value: data)
}
}
/// Rebuilds an immutable value ``Node`` from this subtree, for serialization
/// or value comparison.
var node: PureXML.Model.Node {
switch kind {
case .document:
.document(projectedChildren)
case .element:
.element(PureXML.Model.Element(
name: name ?? PureXML.Model.QualifiedName(""),
attributes: attributes,
children: projectedChildren,
))
case .text:
.text(value)
case .entityReference:
.text(stringValue)
case .cdata:
.cdata(value)
case .comment:
.comment(value)
case .processingInstruction:
.processingInstruction(target: name?.description ?? "", data: value)
case .doctype, .namespace:
// DOM-structural kinds with no place in the content value model; they
// are dropped from a parent's projection (see projectedChildren), so a
// direct call here yields empty text only as a defensive fallback.
.text("")
}
}
/// The value-`Node` children of this subtree: doctype and namespace nodes are
/// dropped (they carry no content), and an entity-reference node is spliced
/// open into its replacement nodes, so the projection is a pure content tree.
private var projectedChildren: [PureXML.Model.Node] {
children.flatMap { child -> [PureXML.Model.Node] in
switch child.kind {
case .doctype, .namespace:
[]
case .entityReference:
child.projectedChildren
default:
[child.node]
}
}
}
}