PureXML/Sources/Model/Element.swift
Mihaela Mihaljevic d27e108b4d
perf(model): make the value Node tree type depth-safe (#344)
Element stored its children inline and Node was `indirect`, so a Node's
recursive layout made building, comparing, hashing, and releasing a deeply
nested tree recurse one native frame per level and overflow the stack
(signal 10) -- on a constrained stack such as a Swift Task, as shallow as a
few thousand deep.

Element now holds its children behind a copy-on-write reference
(ElementStorage), so Node is a fixed-size value (no longer indirect) and:

- `==` and `hashValue` walk the tree iteratively through explicit work
  stacks (the synthesized conformances recursed); equal trees still hash
  equally.
- ElementStorage's `deinit` flattens the subtree into a heap worklist
  before releasing, draining only storage it solely owns so a copy-on-write
  sibling keeps its subtree.
- `children` is a computed copy-on-write view with a `_modify` accessor, so
  in-place mutation (the parser's append loop) stays amortized O(1).

Value semantics are unchanged; Element/Node remain Equatable, Hashable,
Sendable. A 50000-deep tree builds, compares, hashes, and releases in a
fraction of a second; 1782 tests pass. Completes the value-Node half of #341.
2026-06-26 17:05:42 +02:00

88 lines
3.3 KiB
Swift

public extension PureXML.Model {
/// An XML element: a qualified name, ordered attributes, and ordered child nodes.
///
/// A value type with full value semantics: copying an element copies its
/// children (lazily, copy-on-write). The children are held behind a reference
/// (``ElementStorage``) so a deeply-nested tree neither overflows the stack on
/// release nor on equality and hashing, which walk the tree iteratively.
struct Element: Sendable {
public var name: QualifiedName
public var attributes: [Attribute]
/// Copy-on-write backing; `children` is the public, value-semantic view.
var storage: ElementStorage
/// The element's child nodes. Reads share the backing storage; a write
/// first copies the storage if it is shared, preserving value semantics.
///
/// The `_modify` accessor yields the backing array in place (after the
/// copy-on-write check), so an in-place mutation such as
/// `element.children.append(_:)` does not copy the whole array on every
/// call. Without it a computed property would read, mutate a temporary,
/// and write back, making repeated appends quadratic (which is exactly the
/// pattern the parser uses while building an element).
public var children: [Node] {
get { storage.children }
_modify {
if !isKnownUniquelyReferenced(&storage) {
storage = ElementStorage(storage.children)
}
yield &storage.children
}
set {
if isKnownUniquelyReferenced(&storage) {
storage.children = newValue
} else {
storage = ElementStorage(newValue)
}
}
}
public init(
name: QualifiedName,
attributes: [Attribute] = [],
children: [Node] = [],
) {
self.name = name
self.attributes = attributes
storage = ElementStorage(children)
}
public init(
_ name: String,
attributes: [Attribute] = [],
children: [Node] = [],
) {
self.init(
name: QualifiedName(name),
attributes: attributes,
children: children,
)
}
/// The concatenated text of direct text and CDATA children.
public var text: String {
children.reduce(into: "") { accumulated, child in
switch child {
case let .text(value), let .cdata(value):
accumulated += value
default:
break
}
}
}
}
}
extension PureXML.Model.Element: Equatable, Hashable {
/// Value equality over the whole subtree, compared iteratively (see
/// ``PureXML/Model/Node`` `==`) so a deeply-nested element does not recurse.
public static func == (lhs: Self, rhs: Self) -> Bool {
PureXML.Model.Node.treesEqual(.element(lhs), .element(rhs))
}
/// Hashes the whole subtree iteratively, consistent with `==`.
public func hash(into hasher: inout Hasher) {
PureXML.Model.Node.element(self).hash(into: &hasher)
}
}