Third profile-guided pass (#139, docs/benchmarks.md pass 27), targeting
scanContentMarkup(), which inlines the start-tag/attribute/namespace scanners
and carries ~46% of parse time.
scanAttributeValue's opening and closing quote checks ran on the Character
path (peek + advance, buffering the quote as a Character only to compare and
discard it) on every attribute value in the document; both now check the raw
byte first, reaching the byte fast path with no Character buffering, with the
Character path kept as the off-fast-path fallback for streaming sources. The
quote bytes are ASCII and non-CR/LF, so the raw byte and the normalized
stream agree, the same reasoning peekByte's other callers already rely on.
NamespaceContext.checkExpandedNames allocated a Set's native storage and
built a concatenated URI+local key string per element even when at most one
attribute carries a namespace URI, where no duplicate is possible (the
default namespace never applies to attributes, and xmlns declarations carry
no URI). Counting URI-bearing attributes first, with no allocation, and
returning early below two skips all of it on the overwhelming majority of
elements; for xml:* attributes specifically the key exceeded the small-string
limit, so this also removes a real per-element heap allocation.
EntityDecoder.decode/decodeLenient guarded with raw.contains("&"), a
grapheme-by-grapheme walk to find one ASCII byte, once per ampersand-bearing
text node or attribute value; raw.utf8.contains(0x26) finds the same byte
without decoding graphemes, the same shape as checkNameShape's colon check.
The quote-handling extraction into two helpers also resolves the cyclomatic
complexity the inline version tripped. Six new regression tests pin the
quote edge cases (empty value, single-quoted, a quote kind nested inside the
other, unterminated at end of input) plus byte/Character-path agreement for
each in the parser path differential.
Verified: full local suite (1858 tests) and full W3C XSTS suite (14383
groups) pass unchanged. Parse ~6.00x -> ~4.75x versus libxml2 at the
20000-item corpus, best of 5.
Two profile-guided passes (#139), found by sample-profiling a 42MB generated
corpus after re-measuring the parse/libxml2 ratio fresh:
scanAttributeValue's closing-quote check re-derived an already-known answer
through Reader.consume's general String-iteration path, which allocates a
fresh String(quote) and iterates it via Character machinery twice, on every
single attribute value in the document. The content loop's own break
condition already guarantees the buffered peek is either the quote or end of
input, so checking it directly and advancing once is the same behavior for
less work.
NamespaceContext.enterElement, called once per element: checkNameShape's
name.localName.contains(":") resolved to Sequence.contains(_:), walking
extended grapheme clusters to find one ASCII byte; utf8.contains(0x3A) finds
the same colon via the UTF-8 byte view. Separately, attributes.map{
resolveAttribute } allocated a fresh backing array on every element even
though resolveAttribute returns its argument unchanged whenever the attribute
has no prefix or the xmlns prefix (the common case); checking first with no
allocation and reusing the input array when nothing would change gives the
identical result without it.
Both are same-observable-behavior changes, verified against the full local
suite (1852 tests) and the full W3C XSTS conformance suite (14383 groups),
both unchanged. Measured parse ~6.59x -> ~6.00x versus libxml2 at the
20000-item/4.2MB corpus, best of 5; see docs/benchmarks.md passes 25-26.
Each open element resolved its name and attributes by looking up namespace
bindings through NamespaceContext.lookup, which walked a stack of scopes as
deep as the document. With one lookup per element over an O(depth) stack,
parsing a deeply-nested document was O(depth^2): a 40000-deep document took
~39s.
The context now maintains the current in-scope bindings incrementally: a
single prefix-to-URI map, plus a per-element undo list recording the
bindings each element overwrote so closing it restores the enclosing state.
A lookup is O(1) and entering/leaving an element is O(its xmlns
declarations), so parsing is O(n). The same document now parses in ~0.2s.
Namespace semantics are unchanged: the nearest in-scope binding still wins,
an empty default binding still undeclares, and 81 namespace tests plus the
full suite (1784) pass. Closes#345.
The namespace layer rejects malformed qualified names, declarations of
the xmlns prefix, misbindings of the xml prefix and either reserved
namespace name (including as the default namespace), and prefix
undeclaration; attributes must be distinct by expanded name; PI targets
and entity/notation names are NCNames; and DTD-typed xmlns values bind
their normalized form. Clears all 18 namespace cases from the eduni
baseline; xmltest valid/sa/012 joins the documented deviations per the
suite's own NAMESPACE='no' flag.
Closes#136
Add namespace resolution following the XML Namespaces rules, reimplemented in
Swift from libxml2's SAX2 model; no upstream code copied.
- QualifiedName gains an optional resolved namespaceURI (nil for names built
directly or in no namespace), with a resolved(namespaceURI:) helper.
- NamespaceContext maintains a scope stack of xmlns bindings as elements open
and close, resolving element and attribute names against it.
- A default namespace applies to unprefixed element names but never to
attributes; the xml prefix is built in; xmlns declarations are preserved as
attributes so documents round-trip; an unbound prefix raises
ParseError.undefinedNamespacePrefix. End-tag matching stays lexical.
- Namespace-free documents are unchanged (URIs remain nil).
- 7 namespace tests; 39 total.