PureXML/Sources/Parsing/EventReaderContent.swift
Mihaela Mihaljevic ead838f83c
perf(parsing): dispatch element-content markup on the byte after '<' (#276)
The element-content markup dispatch ran up to five literal string
comparisons (</, <!--, <![CDATA[, <?, <!DOCTYPE) before the start-tag
branch won, a per-element cost the parse profile indicted as
Reader.matchesBytes. Branch instead on the single byte after '<' on the
byte fast path: '/' end tag, '?' processing instruction, '!' a
declaration, otherwise a start tag.

The Character path keeps the equivalent matches()-based dispatch for a
streaming source or spliced entity text, and a '<!' that matches none of
comment/CDATA/DOCTYPE still falls through to the start-tag scanner with
the same expectedName error. Measured parse ~9.5x -> ~9.0x slower than
libxml2 on the 20k-item corpus; the byte and Character paths are held
identical by ParserPathDifferentialTests over its adversarial corpus,
and the Xalan, W3C, and RELAX NG suites stay green. No spec behavior
changed.
2026-06-24 19:25:10 +02:00

68 lines
3.2 KiB
Swift

/// Content markup dispatch, in its own file to keep the reader's primary type
/// body under the length cap. Behavior is part of EventReader proper.
extension PureXML.Parsing.EventReader {
/// At a '<' in element content, dispatches to the matching markup scanner.
/// The caller has confirmed the lead is '<' and set the event start. Shared
/// by the byte and Character dispatch paths so the two cannot diverge.
///
/// On the byte fast path the single byte after '<' selects the branch ('/'
/// end tag, '?' PI, '!' a declaration, otherwise a start tag), so an
/// element no longer runs up to five literal string comparisons before the
/// start-tag branch wins. The Character path below is the exact fallback for
/// a streaming source or spliced entity text, and a '<!' that matches none
/// of comment/CDATA/DOCTYPE still falls through to the start-tag scanner,
/// which reports the same `expectedName` error as before.
mutating func scanContentMarkup() throws -> Event {
switch reader.peekByte(1) {
case 0x2F: // '/'
try scanEndTag()
case 0x3F: // '?'
try scanContentProcessingInstruction()
case 0x21: // '!'
try scanContentDeclaration()
case .some:
try scanStartTag()
case nil:
try scanContentMarkupViaCharacters()
}
}
/// The Character-path markup dispatch, used off the byte fast path (a
/// streaming source or spliced entity text). Equivalent to the byte switch
/// in `scanContentMarkup`.
private mutating func scanContentMarkupViaCharacters() throws -> Event {
if reader.matches("</") { return try scanEndTag() }
if reader.matches("<?") { return try scanContentProcessingInstruction() }
if reader.matches("<!") { return try scanContentDeclaration() }
return try scanStartTag()
}
/// Scans a `<!...` construct in element content: a comment, a CDATA
/// section, or a rejected DOCTYPE. A `<!` matching none of these falls
/// through to the start-tag scanner, which reports the same `expectedName`
/// error as before.
private mutating func scanContentDeclaration() throws -> Event {
if reader.matches("<!--") {
recordEmptyElementContent("a comment")
return try scanComment()
}
if reader.matches("<![CDATA[") { return try scanCDATA() }
if reader.matches("<!DOCTYPE") {
throw PureXML.Parsing.ParseError.unsupportedDoctype(reader.mark)
}
return try scanStartTag()
}
/// Scans a processing instruction in element content, rejecting the
/// reserved `xml` target (legal only as the document's leading declaration).
private mutating func scanContentProcessingInstruction() throws -> Event {
let mark = reader.mark
recordEmptyElementContent("a processing instruction")
let instruction = try scanProcessingInstruction()
if instruction.target.lowercased() == "xml" {
throw PureXML.Parsing.ParseError.reservedProcessingInstructionTarget(mark)
}
return .processingInstruction(target: instruction.target, data: instruction.data)
}
}