Two profile-guided serialize passes (#139, docs/benchmarks.md passes 29-30). The attribute writer now precomputes the quote character as a String once per element instead of interpolating a Character into every attribute, avoiding the grapheme-breadcrumb machinery that repeats twice per attribute for a value that never changes within an element. Escaping.comment and processingInstruction's escaping fallback (taken whenever a comment has a hyphen or PI data has a '?', a case pass 8 could skip past but not speed up) is rewritten in the same byte-scan bulk-copy shape as passes 23-24, so a value with one rare trigger no longer pays a full character-by-character rebuild. Both are same-observable-behavior changes. The quote change is byte- identical by construction. The comment/PI rewrite is verified against a new differential suite that reimplements the original character-loop as an independent oracle and checks adjacency/trailing/combining-mark cases plus two 20000-case seeded fuzzes, since a byte match inside a combining-mark cluster is not, on its own, the bare trigger character. Full local suite (1925 tests) unchanged. Measured with interleaved same-state A/B: pass 29 ~1.5% at a 100k-item corpus; pass 30 ~2x on a dedicated comment/PI micro-benchmark (the standard corpus has no comment/PI content, so real documents with copyright banners or hyphenated comment prose see this, not the headline ratio). A same- session multi-append restructuring attempt measured a clear ~6% regression and was reverted; a suspected hasInlineContent double-scan was profiled at ~0.2% of samples and not worth a pass. Both outcomes are recorded in docs/benchmarks.md rather than dropped.
121 lines
4.3 KiB
Swift
121 lines
4.3 KiB
Swift
import Testing
|
|
@testable import PureXML
|
|
|
|
/// Reference oracle for `Escaping.comment`/`processingInstruction`: the original
|
|
/// character-by-character algorithm the byte-scanning bulk-copy rewrite must
|
|
/// match exactly, including when a hyphen/`?`/`>` combines with a following
|
|
/// combining mark into a single extended grapheme cluster (a byte match that is
|
|
/// not, on its own, that bare character).
|
|
private enum ReferenceEscaping {
|
|
static func comment(_ value: String) -> String {
|
|
var result = ""
|
|
var previousHyphen = false
|
|
for character in value {
|
|
if character == "-", previousHyphen {
|
|
result.append(" ")
|
|
}
|
|
result.append(character)
|
|
previousHyphen = character == "-"
|
|
}
|
|
if previousHyphen {
|
|
result.append(" ")
|
|
}
|
|
return result
|
|
}
|
|
|
|
static func processingInstruction(_ data: String) -> String {
|
|
var result = ""
|
|
var previousQuestion = false
|
|
for character in data {
|
|
if character == ">", previousQuestion {
|
|
result.append(" ")
|
|
}
|
|
result.append(character)
|
|
previousQuestion = character == "?"
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
|
|
private struct DifferentialRNG: RandomNumberGenerator {
|
|
private var state: UInt64
|
|
init(seed: UInt64) {
|
|
state = seed == 0 ? 0x9E37_79B9_7F4A_7C15 : seed
|
|
}
|
|
|
|
mutating func next() -> UInt64 {
|
|
state ^= state >> 12
|
|
state ^= state << 25
|
|
state ^= state >> 27
|
|
return state &* 0x2545_F491_4F6C_DD1D
|
|
}
|
|
}
|
|
|
|
@Suite("Comment/PI escaping: bulk-copy rewrite matches the character-by-character oracle")
|
|
struct CommentAndPIEscapingDifferentialTests {
|
|
private typealias Escaping = PureXML.Emitting.Escaping
|
|
|
|
private static let combiningMarks: [Character] = ["\u{0301}", "\u{0332}", "\u{20D0}"]
|
|
|
|
@Test("comment: hand-picked adjacency and combining-mark cases")
|
|
func test_comment_handPicked() {
|
|
let cases = [
|
|
"", "-", "--", "---", "----",
|
|
"a-b", "a--b", "a---b", "a----b",
|
|
"-a", "--a", "a-", "a--",
|
|
"trailing-", "trailing--", "trailing---",
|
|
"-\u{0301}", "-\u{0301}-", "a-\u{0301}-b", "--\u{0301}-",
|
|
"hyphen-\u{0332}ated words are fine-ish- probably",
|
|
]
|
|
for value in cases {
|
|
#expect(
|
|
Escaping.comment(value) == ReferenceEscaping.comment(value),
|
|
"mismatch for \(value.debugDescription)",
|
|
)
|
|
}
|
|
}
|
|
|
|
@Test("processing instruction: hand-picked adjacency and combining-mark cases")
|
|
func test_pi_handPicked() {
|
|
let cases = [
|
|
"", "?", ">", "?>", "??>", "?>>", "??>>", "?a>", "a?>b",
|
|
"?\u{0301}>", "?\u{0301}>b", "a?\u{0301}>", "?>?>?>",
|
|
"?a?b>c", "a>b?c",
|
|
]
|
|
for value in cases {
|
|
#expect(
|
|
Escaping.processingInstruction(value) == ReferenceEscaping.processingInstruction(value),
|
|
"mismatch for \(value.debugDescription)",
|
|
)
|
|
}
|
|
}
|
|
|
|
@Test("comment: seeded fuzz across hyphen/combining-mark alphabets")
|
|
func test_comment_fuzz() {
|
|
var rng = DifferentialRNG(seed: 12345)
|
|
let alphabet: [Character] = ["a", "b", "-"] + Self.combiningMarks
|
|
for _ in 0 ..< 20000 {
|
|
let length = Int.random(in: 0 ..< 12, using: &rng)
|
|
let value = String((0 ..< length).map { _ in alphabet[Int.random(in: 0 ..< alphabet.count, using: &rng)] })
|
|
#expect(
|
|
Escaping.comment(value) == ReferenceEscaping.comment(value),
|
|
"mismatch for \(value.debugDescription)",
|
|
)
|
|
}
|
|
}
|
|
|
|
@Test("processing instruction: seeded fuzz across ?/>/combining-mark alphabets")
|
|
func test_pi_fuzz() {
|
|
var rng = DifferentialRNG(seed: 67890)
|
|
let alphabet: [Character] = ["a", "b", "?", ">"] + Self.combiningMarks
|
|
for _ in 0 ..< 20000 {
|
|
let length = Int.random(in: 0 ..< 12, using: &rng)
|
|
let value = String((0 ..< length).map { _ in alphabet[Int.random(in: 0 ..< alphabet.count, using: &rng)] })
|
|
#expect(
|
|
Escaping.processingInstruction(value) == ReferenceEscaping.processingInstruction(value),
|
|
"mismatch for \(value.debugDescription)",
|
|
)
|
|
}
|
|
}
|
|
}
|