368 KiB
Changelog
All notable changes to PureXML are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Changed
- The XSLT
system-property('xsl:vendor-url')now returns the project's address on git.aleahim.com, where the repository moved from Codeberg.
Fixed
-
An element restricting a NON-repeating base choice ({max occurs} = 1) is now checked by the literal RecurseAsIfGroup pairing (XSD 1.0 section 3.9.6): the derived element, with its own occurrence range, must be a valid restriction of some branch. The effective-total-range approximation is kept only for a repeating base choice, where the particlesZ001 ambiguity actually lives (occurrence counts accumulated across the choice's own repetitions, which the literal rule cannot express); at {max occurs} = 1 no such accumulation exists and the spec text governs. Closes XSTS particlesHa161 (an optional derived element is not a pointwise restriction of a required branch) and particlesM034 (a derived
d1+cannot map into a branch(d1*, d2+)whose requiredd2is unmapped and not emptiable; the settled-valid twin particlesM035,d2+over the same branch, still passes), taking invalid-schemas-accepted 6 -> 4; stricter than libxml2, which implements neither edge. Separately, particlesIg004 is reclassified as a spec-divergent schema test (4 -> 3): section 3.3.2 mapsblock="#all"and the explicitsubstitution extension restrictionlist to the same {disallowed substitutions} set, so NameAndTypeOK clause 5's superset requirement holds and theinvalidexpectation has no spec path (jointly unsatisfiable with the settled Ig001/002/003 and Ig005 expectations). The remaining three schema deviations (Ik025, Ik027, schN12) are documented policy divergences indocs/xsts-deviations.md, including why src-redefine 6.2.2 cannot be enforced uniformly (mgO013's settled-valid schema violates it). Valid buckets are unchanged at zero. -
A complex-type restriction that swaps an element reference for one of its substitution-group members is now rejected (NameAndTypeOK, XSD 1.0 section 3.9.6 Elt:Elt clause 1: the declared names must match, and no clause grants substitution an allowance in particle restriction). The substitution-group expansion rewrites the compiled term (an abstract head is dropped, members join a synthetic choice), so the checker previously compared the expanded names and the swap looked name-identical;
Particle.declaredReferencenow preserves the pre-expansion identity and the restriction check pairs the declared references first. Closes XSTS elemZ026 and particlesV020 (invalid-schemas-accepted 8 -> 6); stricter than libxml2, which does not implement this edge. particlesZ008, byte-for-byte the same construct with a settledvalidexpectation contradicting elemZ026's settledinvalid, is reclassified as a spec-divergent schema test with the adjudication recorded in the suite. Valid buckets are unchanged at zero.
Changed
-
A fifth serialize-path optimization pass (#139,
docs/benchmarks.mdpass 31):textNeedsEscaping/attributeNeedsEscapingand thecomment/processingInstructionexistence fast-paths now scan a raw contiguous buffer (String.utf8.withContiguousStorageIfAvailable) instead of going throughUTF8View's defaultCollectioniteration, falling back to the original scan when no contiguous storage is available (every string this library builds is natively contiguous, so the fallback is believed dead in practice). Found by profiling a new, deeply-nested/attribute-dense corpus the standard flat catalog corpus has no density to indict; a standalone micro-benchmark confirmed the raw-buffer scan ~2-2.75x faster across several call shapes, order-swap-checked against warm-up bias. Two abstraction attempts were tried and rejected: a shared generic helper taking a predicate closure recovered most but not all of the win, and calling a separate small function from inside the buffer loop (to dodge a cyclomatic-complexity lint violation) erased nearly the entire win even though it is not a closure value; the violation is instead resolved by splitting each function into a contiguous/collection pair, so the call boundary sits outside the loop rather than inside it. Output is byte-identical by construction (same predicate, same trigger bytes, only the iteration mechanism changed); full local suite (1925 tests) and full lint unchanged. Measured with interleaved same-state A/B under real concurrent machine load (load average ~2.4): clean with zero overlap across 5 pairs at the new attribute-heavy nested corpus (~9%); noisier at the 100k-item standard corpus (2 of 5 pairs individually reversed by a small margin) but the means still separate clearly (~4%), the same trust-the-aggregate-under-diagnosed-noise call this log made for pass 28. -
Two serialize-path optimization passes (#139,
docs/benchmarks.mdpasses 29-30), found by sample-profiling the serialize loop: the attribute writer precomputes the quote character as aStringonce per element instead of interpolating aCharacterinto every attribute (aCharacterinterpolation goes through the string's grapheme-breadcrumb machinery, work repeated twice per attribute for a value that never changes within an element), andEscaping.comment/processingInstruction's escaping fallback (taken whenever a comment has a hyphen or PI data has a?, a case pass 8 could only skip past, not speed up) is rewritten in the same byte-scan bulk-copy shape as passes 23-24, so a comment or PI 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 (aCharacterand its single-scalarStringrender identically); the comment/PI rewrite is verified against a new differential suite (Tests/CommentAndPIEscapingDifferentialTests.swift) that reimplements the original character-loop as an independent oracle and checks hand-picked 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 (clean, two pairs); pass 30 ~2x on a dedicated comment/PI micro-benchmark (no comment/PI content exists in the standard corpus, so it does not move the headline ratio there, but real-world documents with copyright banners or hyphenated prose in comments exercise it directly). A same-session multi-append restructuring attempt (splitting a single interpolatedoutput +=into several direct appends) measured a clear ~6% regression and was reverted rather than landed; a suspectedhasInlineContentdouble-scan of an element's children was profiled and found to contribute ~0.2% of samples, not worth a pass. Both outcomes are recorded indocs/benchmarks.mdrather than silently dropped. -
A fourth parse-path optimization pass (#139,
docs/benchmarks.mdpass 28):scanAttributes' duplicate-name check reuses one stored scratchSet<String>(cleared withremoveAll(keepingCapacity: true)per element) instead of allocating fresh native Set storage for every element with attributes. Safe becausescanAttributescannot re-enter itself; duplicate detection, recovering-mode behavior, and thrown errors are unchanged (full local suite and full W3C XSTS pass unchanged). Measured ~2-4% on parse with an interleaved same-state A/B (both patched runs beat both baseline runs), the protocol chosen because the session had concurrent machine load. Two companion outcomes from the same profiling round, recorded rather than hidden: a children-capacity reservation candidate measured neutral and was reverted per the benchmarks log's ship-only-measured-wins rule, and theTreeNodeweak-parent side-table cost (~9% of parse) is documented indocs/benchmarks.mdas a structural finding that needs a design decision, not an optimization pass, sinceparent's nil-after-release semantics are documented public API. -
A third parse-path optimization pass (#139,
docs/benchmarks.mdpass 27), found by sample-profilingscanContentMarkup()in isolation after passes 25-26 (it dominates parse time, ~46%, since it inlinesscanStartTag/scanAttributes/scanAttributeValue/NamespaceContext.enterElement):scanAttributeValue's opening and closing quote checks now use the byte fast path (peekByte()+reader.consume(_:)/a byte peek before the Character peek) instead of the Character path, for every attribute value in the document;NamespaceContext.checkExpandedNamesskips itsSet<String>allocation and key-string building entirely when fewer than two attributes carry a namespace URI (the check can only ever throw on a collision between two such attributes, so 0 or 1 can never trigger it, and this is the overwhelming majority of elements); andEntityDecoder'sraw.contains("&")guard is now a UTF-8 byte scan instead of aCharacter-based one, the same shape asNamespaceContext.checkNameShape's colon check in pass 26. All three are same-observable-behavior changes, verified against the full local suite (1858 tests, 6 new covering the quote-handling edge cases directly: empty value, single-quoted, a quote-kind nested inside the other, and unterminated-at-end-of-input, plus byte/Character-path agreement for each) and the full W3C XSTS suite (14383 groups), both unchanged. Measured parse ~6.00x -> ~4.5-4.75x versus libxml2 at the 20000-item/4.2MB corpus, best of 5; seedocs/benchmarks.mdfor the full before/after and reasoning. -
Two parse-path optimization passes (#139,
docs/benchmarks.mdpasses 25-26), found by sample-profiling a 42MB generated corpus after re-measuring the parse/libxml2 ratio fresh rather than trusting the doc's last-recorded numbers:scanAttributeValue's closing-quote check now compares the already-buffered peek directly instead of re-deriving the same answer throughReader.consume's generalString-iteration path (which allocated a freshString(quote)per attribute value in the document), andNamespaceContext.enterElementscans a name's UTF-8 bytes instead of its Characters to find an illegal embedded colon, and skips its per-element attribute-array allocation entirely when no attribute actually needs namespace resolution (the common all-unprefixed-attributes case). Both are same-observable-behavior changes (verified with the full local suite, 1852 tests, and the full W3C XSTS suite, 14383 groups, both unchanged) that measured parse ~6.59x -> ~6.00x versus libxml2 at the 20000-item/4.2MB corpus, best of 5; seedocs/benchmarks.mdfor the full before/after and the reasoning behind each change. -
Update Codeberg links after the NodetteHQ transfer, including README badges, support and roadmap issue URLs, and the XSLT
system-property('xsl:vendor-url')result. -
Rebuild the issue tracker on Codeberg after the GitHub tracker loss: the production-readiness epic, both XSTS gate trackers, the performance epic, and the Xalan burn-down are recreated with current dated statuses, and
docs/roadmap.mdandREADME.mdnow cite the new numbers. The stale baseline figures in both documents are corrected to the enforced ratchet inTests/XSTSSuiteTests.swift(0 / 8 / 0 / 4); shipped items (located diagnostics, the schema differential harness) no longer show as open.
[0.4.8] - 2026-07-05
Changed
-
A generated-value facet differential against libxml2 (
FacetValidationDifferentialTests, opt-in viaFACET_DIFFandxmllint) exercises the datatype and facet value space, the largest correctness surface the determinism and instance-content differentials do not touch: for each generated simple type restricting a built-in (xs:int,decimal,double,string,token,boolean,date,dateTime,nonNegativeInteger) by a subset of its facets (min/maxInclusive/Exclusive, length/min/maxLength, totalDigits/fractionDigits, pattern, enumeration, whiteSpace), it validates a pool of values chosen at and around the facet boundaries with PureXML and with libxml2 and flags every disagreement, asserting the cardinal rule (zero values PureXML rejects that libxml2 accepts). Over about ten thousand values it found one false positive, theNaNenumeration fixed below, and otherwise zero; the residual false negatives are contested datatype readings (whitespace collapse,NaNagainst a range facet) where PureXML matches the XSD spec and this libxml2 build is stricter. Test-only. -
Compiling a schema with a long
<xs:simpleType>restriction chain (S0restrictingS1restrictingS2, and so on) is no longer near-cubic in the chain length, so a deep chain no longer stalls the compiler, a keystroke-time interactive-safety hang on hostile or generated input. Such a chain is flat in the document, so the same base type was resolved over and over: the named simple types were resolved by an N-pass fixpoint that re-resolved every type on every pass, and the facet-applicability variety check re-scanned the schema's children to find each base at every link of every type's chain. Named simple types are now resolved once in dependency order (base before derived), and the variety check resolves a base through a precomputed name map in constant time and memoizes each type's variety, so a chain is walked once rather than per type. A 600-link restriction chain that took about 4.4 seconds to compile now takes about 30 milliseconds. Behaviour-preserving: the resolved types and the emitted findings are identical, verified conformance-neutral over the full W3C XSTS corpus in release (all four buckets unchanged), the libxml2 determinism differential unchanged, and the full suite. The analogous repeated re-scan in attribute-group reference chains is milder and remains, tracked. -
Compiling a schema with a long
<xs:complexType>simpleContentextension chain (T0extendingT1extendingT2, and so on) is likewise no longer near-cubic. Each of the N types re-walked its whole base chain to resolve the simple type itssimpleContentderives from, the same flat-document re-resolution. The resolved simple type of a complex type'ssimpleContentis now memoized by name (SimpleContentMemo, a reference type that survives the value-type context copies), so a chain is walked once rather than per type. A 400-link chain that took about 23 seconds to compile now takes about 1.2 seconds; past the content-model expansion-depth bound (512) such a chain is rejected with a located error as before. Behaviour-preserving: the resolved type is intrinsic to the name for an acyclic chain (a derivation cycle is rejected elsewhere), verified conformance-neutral over the full W3C XSTS corpus in release. The attribute flattening that accumulates each type's attributes along the chain is inherently quadratic in the bounded chain length and is tracked. -
A generated-instance validation differential against libxml2 (
InstanceValidationDifferentialTests, opt-in viaINSTANCE_DIFFandxmllint) exercises the per-instance content matcher (the graph-structured-stackContentRTNMatchernow in production) over the open input space: for every generated, both-engines-compile schema it validates many generated<doc>instances with PureXML and with libxml2 and compares the verdicts. Where the schema-compile determinism check has its own differential, this characterises instance validation, the cardinal rule's instance half, beyond the finite XSTS corpus. It asserts the cardinal rule (PureXML never rejects an instance libxml2 accepts) and reports false negatives for triage. Over 6568 generated validations (1357 both-valid, 5211 both-invalid) across sequences, choices,allgroups, group references, occurrences, and wildcards it found zero false positives and zero false negatives, independent confirmation that the matcher agrees with libxml2. Test-only; no shipped behaviour change. -
The graph-structured-stack content matcher's runtime resource use now has a proven, asserted bound:
ContentRTNMatcherexposespeakLiveConfigurations(the largest live-configuration set in any closure) andgssNodeCount(the total graph-structured-stack nodes built), andContentRTNDifferentialTestsasserts they stay linear in a shared-group chain's depth (measuredgss = K + 2, peak configurations= 3K + 3), where an explicit return stack would reachO(distinct stacks). The bound is checked by count, not by a machine-dependent wall-clock timeout, so a regression to super-linear fails the test rather than only slowing down. Test-and-metadata only; no shipped behaviour change. -
A generated-schema determinism differential against libxml2 (
DeterminismDifferentialTests, opt-in viaDETERMINISM_DIFFandxmllint) fuzzes content models, elements, group references, choices, sequences,allgroups, occurrences, and wildcards, and compares PureXML's Unique Particle Attribution accept/reject verdict to the reference's. The XSTS corpus is finite and did not exercise the multiply-referenced-group overlap that was a real gap; this characterises the UPA check over the open shape space instead, asserting the cardinal rule (zero schemas PureXML rejects as ambiguous that libxml2 accepts) and reporting false negatives for triage. It found zero false positives and a genuine, named under-rejection: a repeating model whose optional trailing particle and leading particle share a name ((c, b?, c?)*), where the compositional check forms the internal gap before the enclosing repetition's loop edge, so the combined decision set is never checked. Test-only; no shipped behaviour change. -
A model
Groupnow carries an internaldefinitionID, a stable identity stamped on each<xs:group>expansion (from the existing per-compile memo key) and shared by every reference to it. It is metadata only (Groupis notEquatable/Hashable, so it perturbs no comparison) and has no consumer yet: it is the first step of replacing the per-instance content-model build's per-reference group inlining (the2^Kdoubling-chain hang) with the graph-structured-stack matcher designed indocs/design/counted-content-automaton.md. No behaviour change; the XSTS buckets are unchanged. -
The counted content matcher gains a shared-group form:
ContentRTNBuildercompiles a particle to a Recursive Transition Network in which eachdefinitionIDgroup is compiled once and reached by acall, andContentRTNMatchersimulates it with a graph-structured stack (configurations are(state, counters, gssNode); acallsaves the caller's counters on an interned GSS node and areturnrestores them, so a multiply-referenced group never mixes counter state across its callers). This replaces the inlining matcher's2^Kstate and reset-action blow-up with anO(groups)build: a depth-24 doubling chain (2^24inlined states) compiles to under 480 states in under a millisecond. The matcher exposes the full surface the content-model validator reads:matchesWhole,matchedParticles, and the streamingstartStates/step/admissible/isCompleteused for located diagnostics and the "expected a, b" hints. It is verified against the trusted inlining matcher (ContentNFA) as the oracle by a differential gate (ContentRTNDifferentialTests) that locks not only the accept verdict and the matched particle but the located rejection index, theadmissibleexpectation set at every prefix, andisComplete, over: the hand-written model cases, the named-over-wildcard tie-break, the shared-group doubling chains that exercise call and return, and a 4000-case fuzz of nested models with occurrences, wildcards, and group references stamped the way the parser stamps them (a unique id per distinct group, a shared id only for genuinely shared structure, some anonymous inline groups). It is built alongside the inlining matcher and is not yet wired into validation, so there is no behaviour change and the XSTS buckets are unchanged; the switch-over is gated on this differential plus XSTS neutrality. -
Content-model validation and completion now run on the graph-structured-stack matcher: the three call sites that built the inlining automaton per validated element,
ComplexValidatorContentModel.sequenceStructureErrors(located structure errors),ComplexValidator.validateParticle(per-child particle attribution), andXSDCompletions(the acceptable-next labels), now buildContentRTNBuilderand runContentRTNMatcher. This removes the last per-instance2^Kcontent-model blow-up: a schema whose nested<xs:group>references inlined to2^Kpositions (theg{i}=(g{i+1},g{i+1})doubling shape) no longer makes each keystroke-time validation of an element exponential in the nesting depth; the automaton is now built inO(groups). Verified conformance-neutral on the full W3C XSTS corpus in release (valid-schemas-rejectedandvalid-instances-rejectedstay 0,invalid-schemas-acceptedandinvalid-instances-acceptedunchanged at 8 and 14) and behaviour-identical on the located diagnostics and completions by the surface differential above.ContentNFABuilder/ContentNFAare retained as the differential oracle. -
The schema-validity differential harness (
XSTSDifferentialTests, the opt-in libxml2 characterisation for production-readiness stopper 3) now reports each disagreeing schema by its path relative to the corpus root, not its bare basename. The same basename recurs across subdirectories (Facets/Schemas/gMonth_maxInclusive001.xsd, which carriesvalue=""and is correctly rejected, versusFacets/gMonth/gMonth_maxInclusive001.xsd, which carries the contested 2001-REC--MM--form), so a basename-only entry hid which file actually disagrees. The header now also records the triage of the "libxml2 stricter" queue: it is dominated by libxml2 over-strictness (amaxOccurslike999999999999999is a valid unbounded-precision integer libxml2 rejects on its machine-int limit; an unresolvableinclude/importit fails to load) and contested datatype-version readings (gMonth --MM--, accepted to keep valid XSTS instances;anyURIstrictness), not clean false negatives. Characterisation only; no shipped behaviour changes.
Fixed
-
An unprefixed
xsi:typevalue now resolves in the instance's namespace context rather than matching a namespaced type by local name alone.xsi:typeis aQName: a prefix binds through that prefix, an unprefixed name through the in-scope default namespace, and with no default namespace an unprefixed name is in no namespace. PureXML stores each named type under both its namespaced key and a bare local-name alias (for lenient lookup), so an unprefixedxsi:type="Test"matched a{targetNS}Testthrough the alias even when the instance had no default namespace, wrongly typing an otherwise-undeclared root and accepting the document. The resolver now keys an unprefixedxsi:typeto the in-scope default namespace (none here), soxsi:type="Test"names no-namespaceTest, which does not exist; the root element then has neither a declaration nor a usable type and the instance is invalid (W3C XSTS targetNS00101m; libxml2 rejects it identically). A prefixedxsi:typeand an unprefixed one under a matching default namespace are unaffected. Verified over the full W3C XSTS corpus in release:invalid-instances-accepted6 -> 5,valid-instances-rejectedheld at 0 across all 14383 groups, and the full suite passes. -
An XSD
key/keyref/uniquefield that selects a node whose declared type is not simple is now rejected (cvc-identity-constraint.3), rather than wrongly accepted. The XSD identity-constraint rule requires each field to identify a node of simple type; a field that reaches an element declared with no type (the ur-typeanyType) or an inline complex type without simple content does not, so the instance is invalid (the reference, libxml2, rejects it identically). PureXML accepted such instances because its identity validator could resolve a field target's type only for a global element reached by a direct-child name, not for a local element reached through a path (the failing W3C XSTS case idZ010 nestsanyTypefields several levels deep). A compile-time pass (XSDParser.identityNonSimpleFieldKeys) now resolves each field's target element through its selector and field paths, by element-name steps, and records the fields whose declared target is provably non-simple; the validator rejects those at runtime unless the instance gives the node a simple type throughxsi:type(cvc reads the node's governing type, so a text-onlyanyTypeelement withxsi:type="xsd:int"is still a valid field). The resolution is conservative: any wildcard, descendant axis, predicate,ref, or named non-builtin type leaves the field unflagged, so a valid field is never rejected. Verified over the full W3C XSTS corpus in release:invalid-instances-accepted8 -> 7 (idZ010 now rejects),valid-instances-rejectedheld at 0 across all 14383 groups (no false positive introduced), and the full suite passes. -
The XSD regex escapes
\iand\cnow use the XML 1.0 Second Edition name classes (Appendix B), not the parser's Fifth Edition Name production. XSD 1.0 (Datatypes Appendix F) defines\ias the characters that may begin an XML Name (Letter | '_' | ':') and\cas the characters that may appear in one (NameChar), citing the XML 1.0 Second Edition productions, which are the enumerated Unicode-2.0-era character classes of XML 1.0 Appendix B.\i/\cwere wired toPureXML.Parsing.XMLCharacter, which correctly implements the Fifth Edition production used to parse live XML, but the two genuinely differ: the Fifth Edition admits whole blocks where the Second Edition has gaps, so\cover-accepted, for example the combining markU+0346(COMBINING BRIDGE ABOVE), which Appendix B'sCombiningCharexcludes (its first range ends atU+0345). A newXSDNameCharacter, transcribed clean-room from the Appendix B grammar (identical across editions one through four) and stored as sorted code-point ranges with binary-search membership, now backs both escapes; the parser's own name check is untouched. The two range tables are verified to reproduce the W3C XSTS\i/\cenumerations (reZ005,reZ006, 34516 and 35122 code points) exactly and in both directions, so there is no false-positive surface. Verified over the full W3C XSTS corpus in release:invalid-instances-accepted12 -> 11 (reZ006i, whose value isU+0346, now rejects), with both false-positive buckets at 0. Found by triaging the invalid-instance tail to a single boundary code point (U+0345valid vsU+0346invalid) and reading the XML 1.0 Appendix B grammar. -
The Ethiopic digits
U+1369..U+1371(ETHIOPIC DIGIT ONE..NINE) now match\din an XSD pattern and not\D. XSD 1.0 fixes the regular-expression repertoire at Unicode 3.2 (Datatypes Appendix F), where these nine code points are general categoryNdand each carries a decimal value; a later Unicode reclassified them toNo.XSDCategoryderives\d(== \p{Nd}) from the running platform's category, pinned to 3.2 by an assigned-in-3.2 gate plus a per-code-point override table, but the override table was missing these nine, so\dused the modernNoand rejected them. This was a two-directional bug: a valid instance whose\d-constrained value was an Ethiopic digit was wrongly rejected (the cardinal rule: never reject a valid value), and a\D-constrained value of one was wrongly accepted. They are now pinned toNd, which is the complete set of code points that wereNdat 3.2 and are not today (the reverse direction is empty: every code point that becameNdlater was unassigned at 3.2 and so already reportsCn, verified by diffing the 3.2 and current UnicodeNdsets), so\wis unaffected (an Ethiopic numeral is categoryNeither way, neither punctuation, separator, nor other). Verified over the full W3C XSTS corpus in release:invalid-instances-accepted14 -> 12 (reT17,reT38, the\Dcases, now reject), the matching\dvalid cases (reS17,reS38,reZ004) no longer need their suspect-test exclusion and pass as ordinary agreements, and the two false-positive buckets stay 0. Found while triaging the invalid-instance tail against libxml2 (which rejectsreT17/reT38, the reference-aligned reading) back to the authoritativeUnicodeData-3.2.0; a prior note that had treated these as a disputed spec-divergence was the wrong call and is corrected. -
A value of
NaNis no longer wrongly rejected by anxs:doubleorxs:floatenumerationthat listsNaN. Value-space equality (OrderedValue.==) compared with IEEE equality, under whichNaN != NaN, so aNaNvalue never matched aNaNenumeration member and was rejected, a false positive (the cardinal rule: never reject a valid value). The comparison now treatsNaNas identical to itself, the value-space identity the reference (libxml2) and XSD 1.1 use throughout, so<v>NaN</v>validates against a type restrictingxs:doubleby<xs:enumeration value="NaN"/>;INF/-INFalready matched. This equality also backs thekey/uniqueidentity constraints andfixed, where the same identity holds (verified againstxmllint): twoNaN-valued unique fields collide as duplicates, andfixed="NaN"matchesNaN. The ordering facets (minInclusiveand friends) use the order relation, in whichNaNstays incomparable, and are unaffected. Found by the new generated-value facet differential against libxml2, with the key/fixed behaviour confirmed after an adversarial critic flagged the shared comparison; the four W3C XSTS buckets held and the full suite passes, with regressions inSchemaDoubleEnumerationTests. -
A long
<xs:attributeGroup ref>chain (ag0referencingag1referencingag2, and so on) no longer overflows the stack or hangs the compiler. Such a chain is flat in the schema document, so flattening a complex type's attributes through it recursed once per link, overflowing the stack past about six thousand links and running near-cubic before that (about 17 seconds at three thousand links). The attribute-group reference depth is now bounded bymaximumContentModelExpansionDepth(512, the same bound the content-model expansion uses), since the cycle-guardvisitedset already counts the chain depth: a chain past the bound is rejected with a located error (attribute group reference nesting exceeds the maximum depth of 512) rather than crashed. Behaviour-preserving for every schema within the bound, which no realistic schema or the XSTS corpus approaches; verified conformance-neutral over the full W3C XSTS corpus in release (all four buckets unchanged) and by the full suite. Disclosed residual: a schema with very many distinct over-bound attribute-group chains still rejects in time quadratic in the bounded depth per group, because the attribute flattening is re-done per group; memoizing it, as thesimpleContentresolution now is, is the linear follow-up. -
The schema compiler no longer overflows the stack on a content model whose expanded depth is unbounded. Two constructs are flat in the schema document, so they escape the parser's element-nesting
maxDepth(256) yet make a content model arbitrarily deep once compiled: a<xs:group ref>chain (the group definitions are siblings, not nested) and acomplexContentderivation chain (B0extendsB1extendsB2, and so on, the type definitions also siblings). The particle compiler inlines each group reference and composes each derivation once per link, so a long chain recursed once per link until the stack overflowed while compiling a legal, if hostile, schema, a keystroke-time interactive-safety crash. The expanded depth is now bounded by a single counter (XSDContext.expansionDepth, incremented at each compositor level, group-reference expansion, and derivation step) againstmaximumContentModelExpansionDepth(512, twice the element-nesting limit: room for a maximally nested model plus as much again for expansion). A model past the bound is rejected with a located error (content model nesting ... exceeds the maximum depth of 512, or the derivation form), not crashed: a bound, not a band-aid. The bound is on total expanded depth, so it covers both a long chain of shallow links and a short chain of internally deep ones. Together with the determinism-check fix below, this closes the group-reference-chain crash on every compiler pass that recursed on the chain. Verified conformance-neutral over the full W3C XSTS corpus in release (all four buckets unchanged; no valid schema newly rejected, the corpus's content models nesting far below the bound), the libxml2 determinism differential unchanged, and the full suite. An adversarial over-rejection critic confirmed the counter lives in a value-type context whose copies make it track depth, not width, so a wide-but-shallow model (hundreds of sibling particles) is never falsely rejected, and that the rejection is harvested into the schema's errors rather than silently truncating the model. -
A deeply nested chain of
<xs:group>references (g0referencingg1referencingg2, and so on) no longer overflows the stack in the Unique Particle Attribution determinism check. Such a chain is flat in the schema document, the group definitions being siblings rather than nested, so it is not bounded by the parser'smaxDepth; the compositional check summarised each group lazily by recursing through the reference at its use site, so a long chain recursed once per link and overflowed the stack while compiling a legal (if hostile) schema, a keystroke-time interactive-safety crash on the primary platform. Group summaries form an acyclic graph (a circular group reference is rejected elsewhere), so they are now precomputed bottom-up in dependency order: every reachable group is summarised once, in an order where each group's own references are already cached, so the walk's group lookup is always a cache hit and recursion is bounded by a single group's model nesting (whichmaxDepthbounds), not by the chain length. A group summary is context-independent, so this only reorders the work; the set of decision sets it produces is identical and the accept/reject verdict does not depend on their order. Verified verdict-equivalent over the full W3C XSTS corpus in release (all four buckets unchanged:valid-schemas-rejectedandvalid-instances-rejectedstay 0,invalid-schemas-accepted8,invalid-instances-accepted14), the generated-schema determinism differential against libxml2 unchanged (zero false positives, the same three named false negatives), and the full 1816-test suite. The particle-compiler pass that inlines a group reference, which this left recursing on the same chain, is now bounded as well (see the content-model expansion-depth entry above). -
A repeating model whose optional trailing particle and leading particle share a name,
(c, b?, c?)*, is now correctly rejected as a Unique Particle Attribution violation, as libxml2/Xerces reject it: afterbthe nextccould be the trailingc?or the looped leadingc, two distinct particles. The compositional determinism check formed the internal sequence gap before the enclosing repetition's loop edge was added, so the combined{trailing, leading}followpos was never checked (an invalid-schemas-accepted false negative the finite XSTS corpus did not contain). The sequence now carries each trailing end-item's internal suffix in itslastFollow, so a parent gap or the loop edge forms the end-item's full followpos. The carried suffix is always a subset of the true followpos, so the check only catches more genuine conflicts and never over-rejects: the non-repeated(c, b?, c?)and disjoint shapes like(a, b?)*stay valid. Found by the new generated-schema determinism differential against libxml2; verified over the full W3C XSTS corpus in release (valid-schemas-rejected/valid-instances-rejectedstay 0,invalid-*-acceptedunchanged) with an adversarial over-rejection critic finding no valid schema newly rejected over 47 shapes. The related ambiguity through two references to one group in a repeating sequence stays an under-rejection (the cached group identity cannot distinguish a looped from a trailing occurrence without per-occurrence tracking), named inDeterminismDifferentialTests. -
Two references to the same
<xs:group>that overlap ((g?, g?),(g | g),(g*, g)) are now correctly rejected as a Unique Particle Attribution violation, as the inline forms(x?, x?)/(x | x)already were and as libxml2/Xerces reject them. The compositional determinism check summarises each group once and reused the group's node identity across references, so the distinct-identity overlap test could not see that two references are two expanded occurrences, and PureXML accepted these models (an invalid-schemas-accepted false negative). It also let the per-instance content matcher run on such a non-UPA model, where a multiply-referenced nullable group is exponential: a depth-18 optional doubling chain was accepted and then hung instance validation, a keystroke-time interactive-safety hang on a schema PureXML itself accepted. The fix dedupes each decision set (first set, sequence gap,allcross-follow) by particle identity and treats a same-identity collision as the conflict, while the repetition self-loop (a*,(a*)*) keeps its non-flagging dedupe so a single particle looping is not misread as two occurrences. This also bounds the check: the deduped first/follow sets stayO(labels)instead of2^K, so the same depth-40 chain is now rejected at compile in 11 ms and no instance is ever matched against it. A redefine self-extension (<extension base="T">inside the redefinition ofT) lists a type's own content twice througheffectiveModelNodes; that assembly artifact is deduped before the walk so it is not read as a conflict (addB094, schZ007). Verified over the full W3C XSTS corpus in release:valid-schemas-rejectedandvalid-instances-rejectedstay 0 andinvalid-*-acceptedare unchanged (the corpus does not exercise this shape in its pinned buckets), with an adversarial over-rejection critic finding no valid schema newly rejected across the group-overlap shape space. -
XSTS invalid-schemas-accepted (#145): a particle restriction whose restricting element has a union-typed base element now applies Type Derivation OK (Simple) (
cos-st-derived-ok) rather than structural member subsetting. The NameAndTypeOK type clause (§3.9.6) requires the restricting element's type to be validly derived from the base element's type; PureXML accepted a separately declared union whenever its member types were a subset of the base union's members, which is not the spec's derivation relation. The restricting type is now valid over a union base only if it is the base type, derives from it through a declared restriction chain, or (clause 2.2.4) validly derives from one of the union's member types. Catches particlesIk011 (a freshunion(integer)is not a restriction ofunion(integer, string)): invalid-schemas-accepted 9 to 8, with valid-schemas-rejected held at 0 (verified over the full XSTS; the change is scoped to the element-restriction path, so no attribute or simple-type derivation check moved). Disclosed divergence: particlesIk025/Ik027, where the restricting type derives from a union member, are accepted percos-st-derived-okclause 2.2.4, a contested reading their XSTSinvalidexpectation does not share.
Removed
-
Five unused internal schema-consistency wrappers (
idAttributeErrors,consistencyErrors,postNamedTypeErrors,structureErrors,includeCompositionErrors) that returned[String]reason lists, along with the now-orphanedpostCompileErrors(schema:context:containers:namedTypes:)overload and theSchemaCompileNamedTypesstruct it consumed. None had callers; theXSDParser.consistencyErrorsoverload was shadowed by the distinct, still-liveValidation.XSDSchema.consistencyErrors(a different signature returningValidationError), which is why a name-only search first read it as used. The located-diagnostic API (the*Findingsfunctions returningSchemaLocatedFinding, andValidation.XSDSchemareturningValidationError) superseded all of them. Removing them deletes dead code with no behavior change; the live*FindingsandValidation.XSDSchemafunctions are unchanged. A stale doc-comment reference to the renameduserTypeValueConstraintErrors(nowuserTypeValueConstraintFindings) was corrected at the same time. -
Two unused private helpers in
XSLTNamespaceFixup(fixAttributeNameandresolvedPrefix). The result-tree attribute fixup they implemented was inlined intofix(_ element:)and evolved past them (the inlined version additionally shadows a carried prefix throughlocalPrefixes, which the helpers did not), leaving both with zero callers. Removing them deletes dead, diverged duplicate logic with no behavior change; the live inlined path is unchanged. -
Three more dead file-scoped functions found by a private-scope sweep:
selectNodesinXSLTTransformer(its siblingselectXPathNodesis the live one; a doc comment that referenced it was reworded),processingInstructionMatchesinEvaluatorSteps, andparseExternalIDinDoctypeScanner. All three areprivate/fileprivatewith zero callers, so removing them is provably behavior-neutral (the compiler confirms). -
Three more dead functions found by a repo-wide reference sweep:
XSLTTransform.transformToNode(a transform-to-tree variant nothing called; the livetransformreturning a string is independent), andComplexValidatorHelpers.matchesAlltogether with its now-orphaned helperlabel(of:). All had zero references; build and 1775 tests confirm behavior is unchanged.
Fixed
-
The XSLT evaluator is now iterative, so template recursion and result-tree nesting no longer consume the native stack (#356). A recursive named template whose depth is driven by source data (for example
<xsl:with-param name="n" select="count(//x)"/>) could previously recurse without bound and crash the process (SIGBUS) on attacker-controllable input; and even an identity transform of a deeply-nested source overflowed the stack on a small (Swift-concurrency Task) stack. The mutually-recursiveinstantiate/evaluate/applyTemplatesengine is replaced by an explicit work-stack driver (XSLTDriver): each instruction appends its produced items to a sink or pushes sub-work, with the rest of the body pushed first so document order is preserved; template application, the built-in rule's source descent, and result-element nesting all run on the heap. Only stylesheet-bounded helper evaluation (a variable's or parameter's result-tree fragment, an attribute set, anxsl:messagebody) re-enters through a nested driver call, whose native depth is bounded by the stylesheet's own nesting. The result-tree serialization walks the engine now exposes at depth (the namespace fixup, thetextand string-value extractions, and the content-type-meta and CDATA-section rewrites) were made iterative as well. Deep recursion now succeeds on any stack (a 5000-level recursive template builds and serializes on the test Task stack);maxTemplateDepthis retained as runaway protection (an unbounded result tree would otherwise exhaust memory), now defaulting to 40000 and trippingXSLTError.recursionLimitExceededonly past that. Behavior is otherwise unchanged: the full Apache Xalan XSLT 1.0 conformance corpus and the in-repo XSLT/XPath suites pass identically (one previously-failing Xalan case,mdocs07, now passes and was de-baselined).maxTemplateDepthandPureXML.XSLT.defaultMaxTemplateDepth(added for the earlier stack-based guard) keep their public shape. -
Serializing a tree as HTML (
PureXML.HTML.serialize, the html output method's serializer) no longer recurses on the tree's depth, so it no longer overflows the stack on a deeply-nested tree (as shallow as a couple of thousand deep on a Swift Task stack). The serializer now drives a deferred-close work stack (the same shape as the XML serializer andCanonicalizer): an element pushes a close step then its children reversed, and a raw-text element (script,style) flags its direct text children to emit verbatim. The output is byte-identical: the void-element, raw-text, comment, and escaping semantics are unchanged, with the HTML conformance corpus passing. A 50000-deep tree now serializes without crashing. (#350) -
Processing
xi:includeelements (PureXML.XInclude.process) no longer recurses on the tree's depth, so it no longer overflows the stack on a deeply-nested document, even one with no includes (as shallow as a couple of thousand deep on a Swift Task stack). The pass now drives an explicit work stack: each branch gathers its processed children into a reference accumulator, then a deferred build step assembles the node, while thexml:base, inclusion-chain depth, and cycle-detection visited set thread down exactly as before. An include splices its (or its fallback's) content in place by pushing the loaded nodes back onto the stack. Behavior is unchanged: the XInclude/XPointer conformance corpus and the fallback, xpointer, cycle, andxml:basecases pass. A 50000-deep document, with or without an include at its base, now processes without crashing. (#350) -
The two Canonical XML transforms that still recursed on a document's depth are now iterative, so they no longer overflow the stack on a deeply-nested tree (#350). The C14N 2.0 sequential prefix rewrite walks the tree twice (a document-order pass that assigns one canonical prefix per namespace, then a rebuild pass); both now drive explicit stacks, the assign pass a pre-order walk and the rebuild pass a child-gathering accumulator, with the in-scope declarations and the root flag threaded down per element. The node-subset serializer (
canonicalize(_:including:), the C14N form over a selected subtree) now drives the same deferred-close work stack as the main serializer, threading each element's in-scope and rendered namespace context, with an omitted element still passing its declarations to descendants. The canonical bytes are unchanged: the C14N exclusive, 2.0, and node-subset corpora pass. A 50000-deep document now rewrites and subset-canonicalizes without crashing. (#350) -
Canonicalizing a document (
Canonicalizer, the C14N form used for XML signatures) no longer recurses on the document's depth, so it no longer overflows the stack on a deeply-nested document (it crashed around 5000 deep). The serializer now drives a deferred-close work stack that threads each element's namespace context to its children. The canonical bytes are unchanged: the C14N 1.0/1.1 and exclusive conformance corpora pass. (#350) -
Applying DTD attribute defaults to a document (
parseApplyingInternalDTDDefaults, and the defaults pass behind DTD validation) no longer recurses on the document's depth, so it no longer overflows the stack on a deeply-nested document. The tree is rebuilt bottom-up through a new shared iterative spine (Node.rebuildingBottomUp), which the other tree-to-tree transforms will adopt. (#350) -
String-value extraction no longer recurses on a node's depth, so computing the string-value of a deeply-nested node no longer overflows the stack (it crashed as shallow as a couple of thousand deep on a Swift Task stack).
TreeNode.textContentand the value-Nodestring-value behindXPath.Query.strings/Selection.stringValuenow walk the subtree iteratively. (#350) -
The value-
Nodestring-value (behindXPath.Query.stringsandSelection.stringValue) no longer includes the data of descendant comment and processing-instruction nodes in an element's string-value, matching XPath 1.0 (sections 5.1 and 5.5, where only text and CDATA contribute) and the tree-node path. A comment or PI node's own string-value is still its data (5.6, 5.7). -
The value-returning XPath result API (
PureXML.xpath,XPath.Query.evaluate/elements/strings) is now linear for results that nest within one another, not quadratic. Each result was rebuilt as an independent valueNodesubtree, so selecting a deep chain of nested elements materialized the sum of their subtrees. The projection now shares a per-result-set memo keyed by tree-node identity, so a result reuses the already-built valueNodeof a deeper one by reference (copy-on-write); each tree node is projected once.//aover a 8000-deep chain drops from about 19 s to about 0.04 s, and the result set is unchanged. (#348) -
Sorting an XPath result into document order is now linear in the document, not quadratic in its depth. Each node's order key was its root path, an array as long as the node's depth, so ordering the descendants of a deeply-nested document was O(n log n x depth). The key is now a single O(1) index from a per-root pre-order numbering (built once per document and shared through the evaluation cache), with a band ordering an element before its namespace and attribute nodes and its children. A node-set spanning several documents is grouped by root. This linearizes the reference query path (
Query.nodes, which XSLT and Schematron drive) on deep documents; the underlying subtree walk (appendSubtree) is now iterative too, so it no longer recurses. Document order is unchanged. -
Parsing is now linear in nesting depth, not quadratic. Resolving each open element's name and attributes looked up namespace bindings by walking a scope stack as deep as the document, so a deeply-nested document was O(depth^2): a 40000-deep document took tens of seconds. The parser now maintains the current in-scope bindings incrementally (a single map plus a per-element undo list), so a lookup is O(1) and parsing is O(n). The same document parses in a fraction of a second, with no change to resolved namespaces. (#345)
-
The immutable value
Model.Nodetree type is now depth-safe. AnElementpreviously stored its children inline (the enum wasindirect), so aNode's recursive layout made building, comparing, hashing, and releasing a deeply-nested tree recurse one native frame per level and overflow the stack (on a constrained stack such as a Swift Task, as shallow as a few thousand deep).Elementnow holds its children behind a copy-on-write reference (ElementStorage): theNodeenum is no longerindirect, so it is a fixed-size value, and==,hashValue, and release all walk the tree iteratively through explicit work stacks. Release flattens only storage it solely owns, so a copy-on-write sibling keeps its subtree. Value semantics are unchanged (copying a node still copies its children, lazily) andElement/NoderemainEquatable,Hashable, andSendable. A 50000-deep tree now builds, compares, hashes, and releases in a fraction of a second with no crash. (Completes the value-Nodehalf of #341.) -
The mutable
Model.TreeNodetree type is now depth-safe: building one from a valueNode, projecting it back withnode, and releasing it all run in bounded native stack instead of recursing one frame per level. Construction (TreeNode(_:)) and projection (node/projectedChildren) drive explicit post-order work stacks, anddeinitflattens its subtree into a heap worklist before releasing, so a deeply-nested tree no longer overflows the stack through a recursive chain of child releases. The teardown only flattens nodes it solely owns (isKnownUniquelyReferenced), so a child still referenced elsewhere keeps its subtree. A 60000-deep tree releases in well under a tenth of a second; behaviour for ordinary trees is unchanged. The validator walk and the XPath descendant axis (AxisNavigation.appendDescendants) were made iterative for the same reason. (Part of #341. The immutable valueNode's own recursive release, and XPath result ordering/dedup, are still depth-bounded and tracked there.) -
Rejecting a billion-laughs document is no longer exponential in nesting depth. The "does this entity produce markup" check (
splitAtMarkupEntity/producesMarkup) recursed into every reference of every replacement without memoizing, so a chainlol8 -> 10x lol7 -> 100x lol6 -> ...was traversed O(references^depth) times. The check is now memoized per entity name (the property depends only on the fixed entity graph), making it O(entities). A classic eight-level billion-laughs with DTD processing enabled drops from about 18 s to about 0.6 s (now bounded by the expansion budget, not the traversal), with no change to which documents are accepted; a cyclic entity is still rejected. -
The No-Recursion check (
checkRecursion) no longer re-traverses shared sub-chains. It validated the reference graph with only an in-progress (grey) set, so a billion-laughs graph was walked O(references^depth) times; a markup-producing eight-level document took about 31 s to reject. It is now a three-colour DFS that records each fully-verified entity (black set) and skips it on revisit, one pass over the distinct edges. The same document drops to about 11 s; cyclic entities are still rejected and no accepted document changes. (The residual cost is the re-injection re-read of the growing remainder on the markup-inclusion path, whereReader.injectre-maps the unconsumed tail each cycle; tracked in #339.) -
Entity and content expansion is no longer quadratic in the size of a single expansion. The parser's lookahead buffer consumed from the front with
Array.removeFirst()and re-injected entity replacement text withinsert(at: 0), both O(buffer), so a large internal entity (or a long run pushed back by reference inclusion) was consumed in O(n^2). The buffer now advances a head index, so front-consume is O(1), and re-injection reuses the space the consumed prefix freed, so a single large expansion is linear. An 800000-character internal text entity that previously took quadratic time now parses in about 0.6 s. Behavior, characters, and limits are unchanged; guarded by the standing suite. (A separate quadratic in deeply-nested entity references, where the content scanner re-reads the un-expanded remainder, is tracked as its own issue.) -
A complex type with simple content that restricts a base may no longer relax a base
requiredattribute tooptional(XSD 1.0cos-ct-derived-ok/derivation-ok-restriction.2). The attribute-use restriction check ran only overcomplexContentrestrictions, so asimpleContentrestriction that relaxed a required attribute (or added a non-base attribute, or widened the attribute wildcard) was accepted; it now runs over both derivation roots. Closes XSTSparticlesZ030_d, which libxml2 also rejects, so the fix aligns with the reference rather than exceeding it. XSTS invalid-schemas-accepted 10 -> 9 with valid-schemas-rejected held at 0 (full archive). Guarded bySimpleContentAttrRestrictionTests. -
xpointer(string-range(...))no longer uses a quadratic substring search. It matched the search string at every position of each node's value and re-allocated the whole needle-width window per position (O(text * needle), a denial-of-service vector on a long node value with a near-matching needle); it now uses a linear-time Knuth-Morris-Pratt search with the failure function built once. The matches are unchanged: same non-overlapping, left-to-right occurrences, same offset/length. A near-matching needle over a 16000-character node drops from about 0.18 s to about 0.002 s (~100x), and the cost is now linear. Guarded by the new non-overlapping and backtracking cases inXPointerRangeTests. -
The XSLT
id()function no longer scans the whole document ID index on every call. It filtered the entire index bytokens.contains(...)(O(index) per call), so a stylesheet callingid()across a wide document was quadratic; it now looks each requested token up directly (O(tokens)). The result is unchanged: the same elements in document order. A stylesheet resolving 4000id()references against a 4000-entry index drops from about 0.34 s to about 0.03 s (~13x), and the cost is now linear. (key()already used direct lookups.)
[0.4.7] - 2026-06-26
Fixed
- XSD identity-constraint validation (
xs:unique,xs:key,xs:keyref) is no longer quadratic over a wide list of targets. Two scans drove the cost: the duplicate check compared each new value tuple against every tuple already seen (akeyreflikewise scanned every referenced key), and the error-location helper rescanned the parent's whole child list to position each target. The first now keys tuples in aSetby their raw string, which is sound because field equality reduces to a rawstring == stringcomparison for whitespace-preserving lexical types (most ids); value-space types (numeric, boolean, date), whitespace-collapsing types, and QName-bearing values keep the exact pairwise comparison, so which documents pass is unchanged. The second positions each target through a per-parent, per-name index built once. Validating a 16000-item document against anxs:uniquedrops from about 4.0 s to about 0.12 s (~34x), and the cost is now linear. Guarded bySchemaIdentityScaleDuplicateTests(#312).
[0.4.6] - 2026-06-26
Fixed
xsl:numberlevelssingleandmultipleare no longer quadratic when numbering a wide list. Each numbered node was ranked among its siblings by rescanning the parent's child list, so numbering n siblings was O(n^2). The rank now comes from a per-parent, per-count-pattern cache built once (using the transformer's match cache, so a count test is O(1)), making the numbering linear. The numbers are unchanged. Numbering 8000 list items with the defaultxsl:numberdrops from about 3.5 s to about 0.04 s (~90x). Guarded byXSLTNumberSingleTests.
Fixed
xsl:number level="any"is no longer cubic over a wide fan-out. Numbering a node walks the nodes that precede it in document order, and that walk found each preceding sibling by rescanning the parent's whole child list, so visiting k preceding siblings cost O(k^2), and numbering n nodes was O(n^3). It now finds the context's index once and walks the children before it directly, so the walk is linear in the number of preceding nodes and the numbering is quadratic (inherent to counting preceding nodes per numbered node). The numbers are unchanged. Numbering 1000 items drops from about 4.7 s to about 0.7 s (~6x), and the scaling improves from cubic to quadratic. Guarded byXSLTNumberAnyTests.
Fixed
- The
translate()andcontains()/substring-before()/substring-after()string functions are no longer quadratic.translate()scanned itsfromstring for every source character (O(string * from)); it now builds the from-to map once and translates each character with an O(1) lookup. The substring functions matched the needle at every position of the haystack (O(n*m), quadratic and a denial-of-service vector on a repetitive untrusted string); they now use a linear-time Knuth-Morris-Pratt search. The results are unchanged (first-occurrence-wins translation, first-occurrence substring match). A large-alphabettranslate()and a repetitivecontains()each drop from hundreds of milliseconds to under a millisecond at 4k-16k characters. Guarded byTranslateFunctionTestsandSubstringSearchTests.
[0.4.5] - 2026-06-25
Fixed
-
The
following-siblingandpreceding-siblingaxes no longer scan the parent's child list to locate the context node on every call, and fuse the step's node test into the walk. Locating the context among a wide parent's children was a linear scan independent of the result, so the axes were quadratic over many context siblings; the index now comes from the shared per-evaluation sibling-index cache, and a sibling the node test rejects is never wrapped. The selected siblings and their order are unchanged. Over a wide parent the gratuitous per-context scan and the non-match wrapping are removed (the per-context iteration over the siblings themselves is inherent to the axis). Guarded bySiblingAxisTests. -
Sorting many attribute nodes from the same element into document order is no longer quadratic in the attribute count. An attribute's document-order key bands by its index among its owner's attributes, which was found by a linear scan of the attribute list on every attribute, so sorting an element's K attributes was O(K^2). The index now comes from a cached per-owner name-to-index map (built once on the second lookup, mirroring the sibling-index cache). The order is unchanged. Sorting the attributes of an 8000-attribute element drops from about 0.18 s to about 0.01 s (~16x), and the cost is now linear. Guarded by
AttributeDocumentOrderTests.
[0.4.4] - 2026-06-25
Fixed
- The
followingandprecedingaxes no longer rebuild the whole document node list and linearly search it on every context node, which made them quadratic over many context nodes. They now share a per-evaluation cache of each document's ordered node list and a node-to-index map (the document does not change during evaluation), so the list is built once and a node's position is an O(1) lookup;followingadditionally skips the context's contiguous subtree by index arithmetic rather than filtering every descendant out. The step's node test is fused into the walk, so a node the test rejects is never copied out of the shared list (only the matches are materialized). The selected nodes and their order are unchanged. A 200-contextfollowing::query over a 20k-element document drops from about 16 s to about 6 s. Guarded byFollowingPrecedingAxisTests.
[0.4.3] - 2026-06-25
Fixed
-
key(),id(), and the EXSLTset:leading/set:trailingfunctions no longer rescan a node's siblings per comparison, which made them quadratic over a wide flat fan-out. They sorted their result withsorted(by: Node.precedes), which recomputes both nodes' document-order keys on every comparison (each key a linear scan of a wide parent), andkey()additionally deduplicated its matches with a linear membership scan. They now sort throughsortedByDocumentOrder()(each key computed once, behind a shared sibling-index cache) andkey()deduplicates through a set. The selected nodes, their document order, and the deduplication are unchanged. Akey()-driven transform over a 20k-element document drops from about 1.15 s to about 0.43 s (~2.7x); the Apache Xalan gold-output corpus stays byte-identical. Guarded byXSLTKeyIdOrderTests. -
Extracting the string-value of a node-set no longer scans the node's siblings, which made an XSLT transform (or any repeated
string(@x)/value-of) over a wide flat fan-out quadratic.firstInDocumentOrder()computed a document-order key even for a zero- or one-node set, and that key walks the node up to the root taking its index among each parent's children, a linear scan of a wide parent. For a single node the order is irrelevant (the node is trivially first), so it now returns directly without computing the key. A transform that reads a couple of attributes and child values per element over a 20k-element document drops from about 2.3 s to about 0.65 s (~3.5x); the produced output is unchanged (the Apache Xalan gold-output corpus stays byte-identical). Guarded byFirstInDocumentOrderTests.
[0.4.2] - 2026-06-25
Changed
-
Output escaping locates the characters that need escaping by scanning bytes rather than graphemes. The bulk-copy escape loop still iterated
Characters and compared each to&,<,>(a grapheme comparison per character). Every escaping trigger is an ASCII marker, or in ASCII-only mode a non-ASCII lead byte, soEscaping.textandEscaping.attributenow find the next one withvalue.utf8.firstIndex(where:), decoding no grapheme for the run between markers. The marker byte always lands on a grapheme boundary (an ASCII byte is its own scalar and forces a cluster break; the first byte at or above0x80after a boundary is a lead byte), so the string slice and subscript stay valid and a multi-scalar cluster escapes as a unit. The output is byte-identical. Measuredserialize~23% faster on the 20k-item generated corpus via a same-machine-state A/B. Verified by the oracle differential extended with combining-mark, ZWJ-sequence, and regional-indicator clusters (EscapingBulkCopyTests), the Apache Xalan gold-output and C14N suites, and an adversarial grapheme-alignment review. -
Output escaping copies the verbatim runs between escapable characters in bulk instead of rebuilding the value one character at a time.
Escaping.textandEscaping.attributeappended each character on its own (result += String(character)for every unescaped character) when a value needed escaping, so re-escaping a text node that holds a single&rebuilt it grapheme by grapheme. They now copy each run between escapable characters in a single append and rewrite only the markers (&,<,>, the attribute quote and whitespace, and, in ASCII-only mode, non-ASCII characters). The no-escaping-needed fast path is unchanged. The output is byte-identical. Measuredserialize~7% faster on the 20k-item generated corpus (whose decoded text re-escapes an&) via a same-machine-state A/B. Verified by a per-character-oracle differential with multibyte cases (EscapingBulkCopyTests), the Apache Xalan gold-output and C14N canonicalization suites, and an adversarial review including a 400k-input fuzz. -
parseTreeassembles the mutableTreeNodetree directly from the event stream instead of building the immutable valueNodetree and converting it. The old path allocated two full trees: the parser built a valueNodetree, thenTreeNode.init(_:)walked it to build the reference tree.Parser.buildTreeis the direct counterpart to the value-tree assembler, the same iterative event loop with the same element stack and the same well-formedness guards (it throws on an unmatched end tag and on a document with no element), but it accumulatesTreeNodechildren per frame and adopts them when the element closes, so the tree is allocated once. The produced tree is identical, the same node kinds, names, attributes, values, child order, and parent links. Measuredparse~8% faster on the 20k-item generated corpus via a same-machine-state A/B. Verified by a structural differential against the convert-from-Nodepath (ParseTreeDirectBuildTests), the full suite and the W3C XML conformance, RELAX NG, and Apache Xalan corpora (which all materialize trees throughparseTree), and an adversarial equivalence review. -
The entity decoder finds its markers at the byte level.
EntityExpander.expandlocated the next&,<, and;withString.firstIndexover graphemes, decoding every literal byte of the run into a grapheme just to look for a one-byte marker. All three markers are ASCII, so a UTF-8 index is a valid string index: the scan now runs overraw.utf8and the found index still subscripts the string directly. The decoded text is unchanged, including text where the markers follow multibyte UTF-8 (precomposed, combining, and astral characters all keep the marker on a grapheme boundary). Measuredparse~5.5% faster on the 20k-item generated corpus via a same-machine-state A/B. Verified by the decode differential (EntityDecodeBulkTests, extended with multibyte-boundary cases) and the W3C XML conformance corpora. -
Materializing the mutable
TreeNodetree from a parsed valueNodeadopts each node's prepared children directly instead of appending them one at a time. Building a node ranappendfor every child, which detaches the child from any previous parent, rejects a cycle, and grows the children array element by element. The children produced by the value-Nodeconversion are freshly built and parentless, so those guards are always no-ops and the repeated growth is wasted;TreeNode.init(adopting:)adopts the already-built array and only wires each child's parent. The resulting tree is identical (same children in the same order, the same parent links). The adopting initializer is used only by the conversion; every public factory keeps the detaching, cycle-checkingappendpath for externally supplied children. Measuredparse~12% faster on the 20k-item generated corpus via a same-machine-state A/B. Verified by a parent-wiring differential (TreeNodeAdoptingInitTests), the W3C XML conformance corpora, and an adversarial equivalence review. -
Element and attribute names split their prefix without a second pass over the name. The byte scanner (
takeASCIIName) already visits every byte of an all-ASCII name, so it now also records the offset of the name's first colon at the cost of one comparison per byte.QualifiedName(ascii:colonOffset:)then splits at the known offset, and the common unprefixed name (no colon) is built with no scan at all, whereQualifiedName(_:)previously ranfirstIndex(of:)grapheme by grapheme over the whole name on every element and attribute. The resulting names are identical for every colon placement (none, leading, trailing, middle, doubled). Measuredparse~12% faster on the 20k-item generated corpus via a same-machine-state A/B. Verified by a colon-placement differential (QualifiedNameColonHintTests), the W3C XML conformance corpora, and an adversarial equivalence review. -
The entity decoder copies the literal text between references in bulk instead of one character at a time.
EntityExpander.expandrebuilt every reference-bearing run grapheme by grapheme (result.append(character)with a per-character amplification-budget charge), so a text node holding a single&was reconstructed character by character; it now scans to the next&or<and appends the run in one operation, charging the budget by the run length. The decoded text, the CDATA-verbatim handling, and the billion-laughs budget enforcement are unchanged (the same overrun throws at the same input). The element-content reference-findings pass, which scanned each text run for&and rendered the enclosing element's qualified name, now returns immediately when the document has no DTD content models, which is the common case. Measuredparse~5% faster on the 20k-item generated corpus (whose text uses&) via a same-machine-state A/B. Verified by the W3C XML conformance corpora, a six-case decode differential (EntityDecodeBulkTests: references in runs, decimal and hex character references, declared-entity replacement with budget overrun, CDATA in replacement text, an unterminated reference), and an adversarial equivalence review.
[0.4.1] - 2026-06-25
Changed
-
XPath compiles a step's node test once per query instead of re-evaluating its fixed structure on every node. The descendant and attribute walks tested each candidate through a closure that called
matchesTree/matchesAttributewith the step'sNodeTestand the evaluationnamespacesdictionary, so the dominant traversal of a//name-style query passed (and reference-counted) the namespace dictionary by value on every one of tens of thousands of nodes.compiledTreeTest,compiledAttributeTest, andcompiledNodeTestresolve the parts that do not change as the walk moves (the axis principal kind, whether eval-time bindings are in effect, the prefix-wildcard shape of the test name) exactly once, returning a tight per-node closure that captures only the local name. The selected nodes are identical to the unfusedmatchesTree/matchesAttribute/matchespath. Measuredxpath~35% faster on the 20k-item generated corpus via a same-machine-state A/B (from ~2.0x to ~1.3x slower than libxml2). Guarded byXPathCompiledNodeTestTests, an exhaustive differential over every node, test, axis, and binding asserting the compiled closures equal the functions they specialize, and confirmed by an adversarial equivalence review. -
XPath evaluation shares the focus-independent state (the variable bindings, the function library, the namespace bindings, and the budget) through a single
Environmentreference instead of carrying it by value in every context. Re-focusing the context on each node of a node-set, which happens once per node when a predicate is applied, copied three dictionaries every time; it now copies only the node, position, and size and shares the environment by reference. The values and behavior are unchanged. Measuredxpath~16% faster on the 20k-item generated corpus via a same-machine-state A/B; covered by the full XPath suite (the change is internal to the evaluator). -
The XPath attribute axis fuses the step's node test into the walk, the same way the descendant axes do, so an attribute the test rejects is never wrapped in an
XPath.Node. A predicate like[@name='value']previously built anXPath.Nodefor every attribute of the element (each wrap retains the owner element and copies the attribute's qualified name and value) and then filtered to the one named attribute; it now wraps only the matching attribute.matchesdelegates, for an attribute node, to a newmatchesAttribute(the attribute axis always has the attribute principal kind, so this is exact), and the attribute walk applies it. The node-set, declaration order, and uniqueness are identical to the build-then-filter path. Measuredxpath~20% faster on the 20k-item generated corpus via a same-machine-state A/B. Guarded byXPathFusedAttributeTests(name and namespaced-name tests, the principal-kind wildcard,node(), the kind tests that never select an attribute, and a predicate over the selection) and confirmed by an adversarial equivalence review. -
The XPath descendant and descendant-or-self axes fuse the step's node test into the traversal, so a node the test rejects is never wrapped in an
XPath.Node(and so never reference-counted). Previously the axis built the full subtree node list and then filtered it; for a//namequery over a wide document, most of the wrapped nodes were immediately discarded. The node test now runs against the raw tree node during the walk:matches(_:_:on:namespaces:)delegates to a new tree-nodematchesTree(one source of truth, exercised by the full XPath suite), and the descendant walk applies it. The node-set, document order, and uniqueness are identical to the build-then-filter path; only non-matching nodes are no longer materialized. Measuredxpath~11% faster on the 20k-item generated corpus via a same-machine-state A/B. Guarded byXPathFusedDescendantTests(every node-test kind through the fused path: element and namespaced names, the principal-kind wildcard,text()/comment()/processing-instruction()/node(), and a predicate on the result) and confirmed by an adversarial equivalence review that found no query whose result the change alters. -
The XPath descendant and descendant-or-self axes build their node list through a single shared accumulator instead of concatenating a freshly built array at every node.
descendants(of:)recursed on wrapped nodes and didresult.append(contentsOf: descendants(of: child))at each level, allocating and copying an array per node (roughly O(n x depth) work over the subtree); it now recurses on the raw tree node and appends into oneinoutaccumulator (O(n)). For a//-style query over a wide document this is the dominant cost: measuredxpath~46% faster on the 20k-item generated corpus via a same-machine-state A/B, with the identical node-set in the identical order (the change is purely how the array is built).
[0.4.0] - 2026-06-25
Fixed
- An
xsi:typesubstitution blocked byblock/blockDefaultis now rejected even when the substitute reaches the declared type through a union member rather than along its own base chain (cvc-elt.4.3 with cos-st-derived-OK clause 2.2.4: a type validly derived from a union member is validly derived from the union, and the{disallowed substitutions}apply to the derivation method used to reach that member). Previously the block check followed only the named base chain, found no path from the substitute to the declared union, and stayed silent, so a substitution reaching a union member by a blocked method was wrongly accepted (W3C XSTSelemT074:block="restriction"on the element, thexsi:typerestricting a union member). A name-keyed union-member table is recorded during schema compilation and the block check now follows a derivation through a member; it adds a rejection only on a positively confirmed path, so an unresolved or inline-anonymous member leaves the check silent rather than risk a false positive. This drops XSTSinvalid-instances-accepted15 -> 14 while holdingvalid-schemas-rejectedandvalid-instances-rejectedat 0 (full XSTS archive, the false-positive direction the production bar guards hardest). The member clause is existential (cos-st-derived-OK 2.2.4): the substitution is barred only when EVERY confirmed derivation path to a member is blocked, so a substitute that is itself a union member, or reaches one member by an unblocked method, is accepted whatever other members it also reaches by a blocked method. Disclosed under-rejection (bounded, not silent): only a union's DIRECTmemberTypesare recorded, not flattened, so a substitution reaching the declared union through a member that is ITSELF a union (a nested-union member-of-a-member path) is not blocked; this can only under-reject (accept an invalid substitution), never over-reject, because an unmodeled path yields no confirmed derivation and the check stays silent rather than guess. Guarded bySchemaXSITypeUnionMemberBlockTests, which checks the rejection, that a non-blocked method (and no block) still accepts the valid substitution, and that a substitute which is itself a member is accepted even when listed after another member it derives from by a blocked method (the over-rejection an adversarial review caught and this design rules out).
Changed
-
A single forward-axis location step from one context node no longer sorts its result into document order, because it is already in document order with no duplicates.
AxisNavigationproduces the structural forward axes (child,descendant,descendant-or-self,self,following,following-sibling) in document order, deriving sibling order from the samechildrenarray the order key compares, so a one-step path over any of them needs neither the document-order sort nor de-duplication. (This is the common shape after//xcompiles and fuses to a singledescendant::xstep.) The attribute axis (whose order interleaves with namespace declarations) and the namespace axis (produced in dictionary/hash order), plus every reverse axis (nearest-first), are excluded and still sorted. Measuredxpath~7% faster on the 20k-item generated corpus via a same-machine-state A/B, identical results. Guarded byXPathSingleStepOrderTests(document order for each included axis, reverse axes still normalized, multi-step paths still sorted) and confirmed by an adversarial order-correctness review that found no query whose order or uniqueness the change breaks. -
A path expression's result is no longer re-de-duplicated before sorting.
evaluateStepsalready returns a duplicate-free node-set (each step de-duplicates across its context nodes, or uses an axis whose per-context results cannot overlap), so theSet-based de-duplication that ran before the document-order sort copied every node for nothing; each copy retains the wrapped tree node, whoseweak parentreference forces a side table and the slow reference-counting path. Path and trailing-path evaluation now sort the steps' result directly; unions and filter primaries (which genuinely can carry duplicates) keep the de-duplicating pass. Measuredxpath~9% faster on the 20k-item generated corpus via a same-machine-state A/B, identical results. Covered by the existing node-set de-duplication and union tests (a path still yields each node once). -
XPath compiles
//(the abbreviation fordescendant-or-self::node()/child::X) to a singledescendant::Xstep when the child step's predicates are non-positional, avoiding the materialization of every node in the subtree as an intermediate context (measuredxpath~10x -> ~7x slower than libxml2 on the 20k-item generated corpus via a same-machine-state A/B, identical results; ~54% faster than before this cycle's two XPath changes combined). The two node-sets are equal, so the fusion is transparent, EXCEPT for a positional predicate://x[1]is the firstxchild of EACH parent, not the firstxin document order, whichdescendant::x[1]would wrongly select. The rewrite is therefore applied only when every predicate is provably non-positional: its value is a boolean, node-set, or string (never a number, which XPath reads asposition() = value) and it nowhere referencesposition()orlast(). Anything not provably both is left unfused, so a result can never change. Guarded byXPathDescendantFusionTests(positional forms keep their per-parent meaning, non-positional forms select across the subtree, composition with trailing and nested steps) and confirmed by an adversarial differential review (fused versus a spec-literal unfused reference) that found no query whose result the fusion changes. -
XPath location-step evaluation skips cross-context node-set de-duplication when no duplicate can arise (measured
xpath~13x -> ~10x slower than libxml2 on the 20k-item generated corpus via a same-machine-state A/B, ~28% faster, identical results). A step accumulated each context node's axis results into aSet<PureXML.XPath.Node>to drop nodes reached from more than one context, which hashed and copied every node (the wrappedTreeNodereference is reference-counted, so each copy is ARC traffic). De-duplication is needed only when two distinct context nodes can reach the same node, so it is now skipped on a single context and on the axes whose results are disjoint per context (child,attribute,namespace,self); those produce the identical node sequence theSetwould (every insertion succeeds) without the hashing. TheSetis kept on the axes that genuinely overlap (descendant,ancestor,parent, the siblings,following/preceding). Predicates are applied per context (unchanged) and the path result still passes through the final order-and-unique pass, so semantics are preserved. Guarded byXPathNodeSetDedupTests, which checks that a node shared by many contexts throughancestor/parent/descendantis still counted once, that the disjoint axes return every distinct result, and that an overlapping union still de-duplicates; the full XPath suite and the Apache Xalan XSLT gold-output corpus stay green. -
Serialization roughly halved (measured
serialize~5.5x -> ~2.7x slower than libxml2 on the 20k-item generated corpus,bash scripts/benchmark.sh 20000 5, with byte-identical output). The escaping functions (text,attribute,comment,processingInstruction) rebuilt every value character by character even when nothing needed escaping, which is the common case. They now scan the value's UTF-8 bytes for the escapable bytes (markup characters, the active attribute quote, the whitespace characters, a comment hyphen, a PI question mark, and any non-ASCII byte in ASCII-only mode) and return the value unchanged when none is present, falling back to the original character path only when something must be escaped. Guarded byEscapingFastPathTests, which pins both the unchanged fast path and the escaped slow path for each function, and verified against the Apache Xalan XSLT gold-output corpus (a serialize-heavy differential). No output changed. -
Attribute-value parsing skips the reference-decode pass when the value contains no
&. The byte-run scanner already visits every value byte, so it now notes any&; an ampersand-free value (the common case) returns its normalized run directly instead of calling the entity decoder, which re-scanned the whole value for&before doing nothing. Measured against a same-machine-state baseline, every run came in below every baseline run (~4% parse improvement on the 20k-item generated corpus). The bulk byte-run scanners (contentRunBytes,attributeRunBytes) moved toReaderByteRuns.swiftwith their cursor mutation centralized through a singleadvanceByteRunmethod, keepingReader's position fields write-encapsulated while the file stays under the length cap. The byte and Character paths are held to identical decoded values byParserPathDifferentialTests(extended with ampersand-free, entity, and character-reference attribute cases); no spec behavior changed. -
Parser throughput on plain-ASCII documents roughly halved against the libxml2 reference (measured
parse20.1x -> 9.5x slower than libxml2 on the 20k-item generated corpus,bash scripts/benchmark.sh 20000 5; ~53% wall-clock reduction). The work was profile-driven (macOSsample, not guesswork): the scanner, not tree construction, dominated parse time (SAX is ~84% of parse), and within it the cost was per-CharacterStringmachinery (String.Iterator.next,_stringCompareWithSmolCheck,_uncheckedFromUTF8, bridge-object ARC). The root cause was that a single buffered lookaheadCharacterdefeated every existing byte fast path (each requires an empty buffer), and the content/markup/attribute dispatch all peeked aCharacterbefore scanning. The fix keeps the reader on the byte path: a non-bufferingpeekBytefor dispatch, a byte-modeskipSpace, an all-ASCIItakeASCIIName, anattributeRunBytesbulk scanner (with the 3.3.3 whitespace-to-space transform, deferring any carriage return to the Character path so CRLF is not double-counted), and asawAmpersandflag so ampersand-free text skips reference decoding entirely. The single highest-leverage change was detecting the closing<of a text run viapeekByteinstead of buffering it, which had been cascading the next element's whole dispatch and name scan onto the slow path. The Character path remains the exact fallback for a streaming source, spliced entity text, and any non-ASCII byte; byte and Character paths are kept from diverging byscanContentMarkup(shared dispatch) and byByteFastPathTests, which pins the fast-path/fallback boundaries (non-ASCII names, CRLF and character references in attribute values, ampersand-free vs entity-bearing text, over-long names, raw<in attributes). Verified against the full local suite (1668 tests) plus the Apache Xalan XSLT gold-output corpus, the W3C XML conformance corpora (xmltest, OASIS, Sun, Eduni, IBM, Japanese), and the RELAX NG spec suite, all green; no spec behavior changed. -
Element-content markup dispatch now branches on the single byte after
<(end tag, processing instruction, declaration, or start tag) instead of running up to five literal string comparisons per element before the start-tag branch wins, a per-element cost the profile indicted (Reader.matchesBytes). The Character path keeps the equivalentmatches()-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 sameexpectedNameerror. Measuredparse~9.5x -> ~9.0x slower than libxml2 on the 20k-item corpus; the byte and Character paths are held identical by the differential oracle (ParserPathDifferentialTests). No spec behavior changed.
[0.3.0] - 2026-06-24
Added
-
Added an opt-in DEEP fuzz campaign (
SchemaFuzzTests.test_deepFuzzCampaign, production-readiness stopper #3: correctness bounded, not sampled). The standing fuzz gate samples a fast, hang-guarded 2000/250/600 iterations every CI run; this runs the same streaming-vs-tree differential overPUREXML_FUZZ_ITERATIONSseeds when that variable is set (skipped otherwise), so a long campaign is reproducible without editing the bounds and a divergence reproduces from its seed. It carries no.timeLimit(a 60k-seed run legitimately exceeds a minute); termination stays guaranteed by the engine's own occurrence/state caps. A campaign over 60,000 seeds (plus 20,000 schema/instance and 80,000 parser-byte iterations) found zero streaming-vs-tree divergences, crashes, or hangs. -
Added a standing streaming-vs-tree differential gate (
SchemaFuzzTests.test_streamingMatchesTreeDifferential, related to #194). The streaming XSD validator is a separate implementation of the same semantics as the tree validator (the conformance oracle), so it must agree with it on every input. The gate fuzzes 2000 deterministic schema/instance pairs and asserts that streaming and the tree validator agree on whether each document is valid, and that every tree error appears in streaming's errors (streaming may add errors on an already-invalid document, issue #206, but may never reject a valid document or miss a real error). This runs in the standard test suite on all platforms, so a future change that reintroduces a streaming false positive (three were found and fixed this cycle: #186, #200, #202) fails CI with a reproducible seed, turning streaming-oracle agreement from a manually-hunted snapshot into an enforced invariant. -
Added an opt-in differential schema-validity harness against libxml2 (
XSTSDifferentialTests, production-readiness stopper #3: characterise correctness against an independent reference, not just the labelled suite). Over 9618 corpus schemas PureXML and libxml2 agree on 4912 (3161 valid, 1751 invalid); the 87 where PureXML is stricter were triaged as PureXML being more correct (libxml2 omits facet consistency, Element Declarations Consistent, whiteSpace restriction, au-props-correct, and notation rules), independently corroborating that PureXML does not over-reject. The 117 where libxml2 is stricter are a false-negative triage queue (stopper #2), written with the rest to /tmp/xsts-differential.txt. xmllint and the corpus are used only in the test harness, never the shipped package. -
Added the counted-content-automaton design for the interactive resource-bound arc. The design separates the language-recognition, matched-particle attribution, and UPA/determinism claims; replaces occurrence unrolling with counter scopes; states the target
O(N * S * D)runtime andO(S * D)live memory bounds; and makes deletingoccursUnrollCap,totalStateCap, andpositionCapthe acceptance criterion. -
OpenAPIKit-style validation framework completion:
BuiltinValidationregistry (23 rules), exhaustive fail/succeed isolation tests, configuration pins, public-type and field coverage gates (scripts/check-validation-coverage.sh,scripts/check-validation-fields.sh), warnings channel withstrict:promotion, two-tierValidatordefaults, XSD schema compile rules as composable validations (SchemaCompileValidation), andValidationOutcomenon-throwing accessor.
Changed
-
Attribute-wildcard union and intersection now follow XSD 1.0 §3.10.6 with Errata E1-10, including the "not expressible" cases (false-negative burndown, stopper #2). The
{namespace constraint}model was generalised fromany/##other/set to the four expressible XSD forms:any; a set (with absent); a pair ofnotand a namespace name; a pair ofnotand absent. Bothnotforms exclude absent, so a union admitting "every namespace except a name, plus absent" (E1-10 rule 5.1.3) has no expressible form. PureXML previously widened such a union to##anyand accepted the schema; the newSchemaWildcardExpressibilityrule rejects a complexContent extension whose attribute-wildcard union with its base is not expressible (src-ct.5), closing XSTS wildZ013. The expressible unions are now computed exactly, so a derived type's wildcard admits the correct attributes at instance time, closing wildZ013a and wildZ013d. Net:invalid-schemas-accepted12 -> 11 andinvalid-instances-accepted17 -> 15, withvalid-schemas-rejectedandvalid-instances-rejectedheld at 0 (full XSTS archive); the generalised model is additive for element wildcards (which never produce the new forms). The union/intersection algebra was taken clean-room from Xerces-JXSWildcardDecl(factual rule numbers only). Guarded bySchemaWildcardUnionExpressibilityTestsand the existing intersection tests. Disclosed under-rejection: the intersection-not-expressible case (src-ct.4, twonotforms negating different namespace names within one type, e.g. a type intersecting its own##otherwith a cross-imported attribute group's##other) is not flagged as a schema error; the intersection stays the less-restrictivenot(lhs)over-approximation so a valid cross-namespace attribute is never wrongly rejected (the prior behaviour, kept FP-safe), andsrc-ct.4detection is a deferred rule. -
A negated character-class subtraction now excludes both the negated base and the subtrahend (false-negative burndown, stopper #2). XSD
charClassSub ::= (posCharGroup | negCharGroup) '-' charClassExpris the set difference of the (possibly negated) group and the subtrahend, so for[^cde-[ag]]negation applies to the base first and the subtrahend is removed from that result: the class admits everything except{a, c, d, e, g}.CharClass.matchesapplied the subtraction to the positive membership and negated afterwards, so it excluded only{c, d, e}and wrongly matcheda/g, accepting instances a correct engine rejects. Negation now applies to the base before subtraction. Only negated classes that also carry a subtraction change behaviour. This closes XSTS RegexTest_430 and RegexTest_422, droppinginvalid-instances-acceptedfrom 19 to 17 withvalid-instances-rejectedheld at 0 (full XSTS archive). A regression is guarded byRegexCategoryTests.test_negatedSubtraction. -
An unknown
\p{...}category or block name in apatternfacet is now rejected at schema-compile time (false-negative burndown, stopper #2). XSD 1.0charPropisIsCategory | IsBlock, an exhaustive set: every Unicode general category plus a fixed Unicode-3.1 block list. The block table (CategoryMatcher.blocks) was missing the nine supplementary-plane blocks (Old Italic, Gothic, Deseret, the Byzantine/Musical/Mathematical symbol blocks, CJK Unified Ideographs Extension B, CJK Compatibility Ideographs Supplement, Tags) and the three surrogate blocks, soparseCategorycould not tell a real-but-untabulated block from a bogus name and stayed lenient (RegexError.unsupported), accepting\p{IsaA0-a9}and the schema declaring it. The table is now the complete XSD set (supplementary blocks added with their ranges; surrogate blocks added as valid names whose ranges hold no scalar, so they match nothing), making the recognised set authoritative, so an unrecognised name throws the rejectingRegexError.invalidProperty. The now-deadRegexError.unsupportedcase and theisMalformedPropertyheuristic are removed. This closes XSTS reK88, droppinginvalid-schemas-acceptedfrom 13 to 12 withvalid-schemas-rejectedheld at 0 (full XSTS archive) and the instance buckets unchanged (invalid-instances-accepted19). As a side effect, a pattern naming a supplementary-plane block (e.g.\p{IsMusicalSymbols}) now matches its characters instead of failing to compile. Block names/ranges were taken from the XSD-supported list in Xerces-JToken.java(clean-room: factual data only). Regressions are guarded byRegexCategoryTests(supplementary/surrogate matching, unknown-name rejection) andSchemaPatternFacetTests. -
A
patternfacet whose value contains an unescaped[inside a character class is now rejected at schema-compile time (false-negative burndown, stopper #2). XSD 1.0 Appendix F definesSingleCharNoEscas[^\[\]]and admits a nested class only as a-[...]subtraction, so a[not introduced by-is a syntax error;RegexParserwas treating it as a literal scalar, so[a[:xyz:]compiled and the declaring schema was accepted. The parser now throws a dedicatedRegexError.unescapedClassBracket(kept distinct from the generic, deliberately-lenientbadClassso the change cannot regress a valid-but-unsupported pattern), whichXSDSimpleParser.rejectsPatterntreats as a genuine syntax error. This closes XSTS RegexTest_993 and RegexTest_1477, droppinginvalid-schemas-acceptedfrom 15 to 13 withvalid-schemas-rejectedheld at 0 (full XSTS archive) and the instance buckets unchanged (invalid-instances-accepted19). The escaped form[a\[c]and the-[...]subtraction[a-z-[aeiou]]stay valid; the unterminated case[a[:xyz:now faults on the inner[(more precise than the prior unterminated-class diagnosis). Regressions are guarded byRegexTests.test_unescapedClassBracketandSchemaPatternFacetTests. Contested-corpus note: this also rejects siblings the MS suite marks valid (([[:]+),([[=]+),([[.]+), RegexTest_989/990/991/1473-1475, and the space-bearing subtraction[a - c - [ b ] ]+), but those entries are W3C-queried (Bugzilla #4118) because "valid" contradicts the Appendix F grammar, so they are excluded from the conformance gate; PureXML follows the grammar and the suite's settled position (993/1477 invalid). -
Compiling a schema with deeply nested, multiply-referenced model groups is no longer exponential.
XSDParserParticles.groupReferenceParticleinlined each<xs:group ref>into the compiledParticlecontent model, sog0=(g1,g1), g1=(g2,g2), …built2^Kparticles (measured: depth 14 = 0.21s, depth 20 ~13-18s, growing ~4x per two levels). The expansion is now memoized per (group definition, cycle-visiting set) in a per-compileGroupParticleMemo, so sibling references share it and the build drops toO(groups)(depth 40 now compiles in ~0.016s; previously infeasible). The shared content term is reused by value (copy-on-write), so the compiled model is byte-for-byte the same and validation is unchanged: the XSTS gate is unchanged (invalid-schemas-accepted 15, valid-schemas-rejected 0, instance buckets held) and the streaming-vs-tree fuzz differential still passes. This closes the schema-compile half of the interactive-safety group-blowup (the companion to thepositionCapdeterminism fix); a regression is guarded by a depth-40 compile test. -
The Unique Particle Attribution (UPA / cos-nonambig) determinism check is now compositional and no longer caps or skips.
ContentModelDeterminismdelegates to a newCompositionalDeterminismengine that computesfirst/followlastover PARTICLE identities and summarizes each<xs:group>once, so a multiply-referenced nested group whose inlined Glushkov automaton would be2^Kpositions is checked inO(nodes * particles^2)time instead of being silently skipped. This DELETESContentModelDeterminism.positionCap(the last named interactive-safety resource-bound blocker, production-readiness stopper #4) and the silent under-rejection it caused (stopper #3). The compositional engine was proven verdict-equivalent to the prior inlining position automaton by a differential over the entire XSTS corpus (zero divergences) before replacing it; the XSTS conformance gate is unchanged (invalid-schemas-accepted 15, valid-schemas-rejected 0), and a new test compiles a 2^20-position nested-group schema without hanging and rejects an ambiguous one the old cap would have skipped. The per-particle model (a particle never conflicts with a repetition of itself) and the QName-only overlap (no substitution-group expansion) are preserved, so the false-positive direction stays at zero. -
Schema compile diagnostics are now located:
SchemaError.inconsistentcarries[ValidationError]with coding paths that resolve toTreeNode.sourceRangeon a ranged schema tree (#169). Parse-time facet and structure findings attach to their declaring nodes where possible;SchemaError.inconsistentFindingsexposes the full list. Schematron reuses the sharedvalidationCodingPath()helper. Value-constraint errors now locate to their declaring element or attribute: adefault/fixedon an ID-derived type, a value that is invalid for its built-in, named user, or inline type, and an illegal combination of value constraints each underline the offending declaration rather than the schema root. The structure-pass checkers are likewise located: a facet inapplicable to a list or union variety, a derivation by restriction ofxs:anySimpleTypeand a constraining facet onanySimpleType-based simple content, a top-level definition missing its name or carrying a local-only attribute, a nested named definition, an emptytargetNamespaceorimportnamespace, and an all-group reference with an illegalminOccurs/maxOccurseach underline their own node instead of the schema root. The simple-type-pass checkers join them: a list whose item type is itself a list or a union containing a list, a list declaring both or neither ofitemType/inlinesimpleType, an empty union, a simpleType restriction whose base resolves to a complex type, anitemType/memberTypereferencing a type that isfinalfor list/union derivation, and an attribute typed by a complex type each underline the offendinglist,union,restriction, orattributenode rather than the schema root. The attribute-pass checkers follow: an attribute declaration that lands in the XSI namespace underlines theattribute; a duplicate attribute use or a second ID-typed attribute underlines its declaringcomplexTypeorattributeGroup; an attribute reference that contradicts a globalfixedvalue underlines the reference; and a complex-type restriction that relaxes a base attribute use (making a required attribute optional, narrowing its type unfaithfully, dropping a fixed value, or widening the attribute wildcard) underlines therestriction. The derivation/cycle-pass checkers, which report a cycle by component name over a name-keyed graph, now record the declaring node alongside that graph and underline it: a type derived from itself underlines itscomplexType/simpleType, an element that is a member of its own substitution group underlines itselement, a model group or attribute group that references itself underlines itsgroup/attributeGroupdeclaration, and an all-group referenced inside asequence/choice/allunderlines the offendinggroupreference. The content-derivation-pass checkers (thecos-ct-extends/src-ctbase-kind family) follow: a complexContent extension adding element content over a simpleContent base, a simpleContent restriction whose base is a built-in or user simple type, an unfaithful inline type in a simpleContent restriction, a complexContent base that is a simple type, a simpleContent extension whose base isxs:anyType, and a mismatchedmixedsetting on a complexContent extension each underline theirextension/restrictionderivation node, and an element carrying adefault/fixedvalue over element-only or empty content underlines theelement. The reference-resolution rule is now genuinely located:referenceFindingspreviously wrapped its whole aggregate in.unlocated, so every undeclared-reference error collapsed onto the schema root despite the rule being dispatched as located; each of its six sub-collectors (the core type/base/itemType/memberTypes/ref/substitutionGroup walk, thexs:-namespace builtin check, the simpleContent base-kind check, the un-imported-namespace check, the redefine-component-existence check, and the import-namespace-agreement check) now attaches to its referencing node,import, orredefinecomponent. The extension/substitution-pass checkers follow: anallgroup reached through an extension over a base with content underlines theextension, an anonymous complex-type restriction that is not a subset of its base underlines therestriction, and a substitution-group member whose type is not validly derived from its head underlines the memberelement. The names/UPA/notation-pass checkers follow: a duplicate global component name underlines the second declaration, a duplicate identity-constraint name underlines the second constraint, akeyrefwhoserefernames no key/unique (or whose field arity differs) underlines thekeyref, a Unique Particle Attribution violation underlines the ambiguous content-model group, and a notation missing bothpublicandsystem, a notation with forbidden content, or aNOTATIONenumeration naming no declared notation underline thenotationorenumeration. The composition/redefine-pass checkers complete the burndown (every schema compile checker now reports throughSchemaLocatedFinding, with no.unlocatedfallback left): a resolvedschemaLocationwhose content is not a schema underlines theinclude/import/redefinereference (its node is captured at the load-failure site), a redefined type that does not restrict or extend itself underlines the redefined type, a redefined group or attribute group with a malformed self-reference underlines the offending reference or redefinition, and a redefined attribute group or model group that does not restrict its original underlines the redefinition. -
DTD parse-time validity findings are warnings until strict validation promotes them;
validateAgainstInternalDTDreturns full findings (errors and warnings).
Fixed
-
xsl:namespace-aliasnow keeps the literal (stylesheet) prefix on aliased result elements, attributes, and namespace declarations, remapping only the namespace URI (XSLT 1.0 7.1.1: the result namespace node keeps the prefix used in the stylesheet). The serializer was adopting theresult-prefixas the output prefix, soaxsl:fooaliased to the XSLT namespace came out asxsl:foo; theresult-prefixnow only selects the replacement namespace. Closes Apache Xalan conformance casesnamespace19,namespace23,namespace24,namespace35, andnamespace113. -
xsl:keywith a unionmatchpattern now indexes every branch. The match was turned into a path by prepending//, but for a union likematch="Level1 | Level2"the//applied only to the first branch (//Level1 | Level2), so the second branch became relative to the root and matched nothing, leaving nodes of that type out of the key. The//is now distributed over each top-level union branch (//Level1 | //Level2), with|inside a predicate or function call left intact. Closes Apache Xalan conformance casesidkey45,idkey46,idkey47, andidkey48. -
xml:space="preserve"on a literal result element (or an ancestor) in the stylesheet now keeps that element's whitespace-only text instead of stripping it (XSLT 1.0 3.4). The stylesheet whitespace stripping dropped every whitespace-only run unconditionally, ignoring an in-scopexml:space="preserve"; the nearest ancestor-or-selfxml:spaceis now consulted, sopreserveretains the run whiledefault(or none) still strips it. Closes Apache Xalan conformance casewhitespace20. -
xsl:number level="single"now counts the element that is thefromboundary when that element also matches thecountpattern (XSLT 1.0 7.7). The search for the count node stopped at the nearestfrom-matching ancestor before testing it againstcount, so a pattern likecount="a|b" from="a"produced an empty number for an element inside ana(the count node is thata, which is also the boundary). The count pattern is now tested before the boundary stops the walk. Closes Apache Xalan conformance casenumbering63. -
The
formatattribute ofxsl:numberis now evaluated as an attribute value template (XSLT 1.0 7.7), soformat="{$f}"uses the value of$frather than the literal string. It was stored and used verbatim, so a computed or passed-in format was never applied. Closes Apache Xalan conformance casenamedtemplate12. -
A large integer-valued number now converts to a string with no decimal point (XPath 1.0 4.2). The number-to-string formatter only treated a value below 1e15 as an integer, so an integer such as
1234567890123456(still exact in a double, which represents integers exactly up to 2^53) printed as1234567890123456.0. The integer bound is now 2^63, the range in which the conversion is safe. Closes Apache Xalan conformance casestring132. -
A lone
/(the root node) is now parsed correctly when it is followed by something other than the end of the expression, such as a),],|, or an operator. The XPath grammar makes the relative part after/optional, but the compiler only treated/as the root path at the very end of the expression, socount(/),generate-id(/), and/ = .tried to parse a step from the following token and yielded an empty node-set. The compiler now recognizes a/as the root whenever no location step follows it. Closes Apache Xalan conformance casesposition106andmdocs15. -
A template match pattern written with an explicit
child::orattribute::axis now gets the default priority of its node test (XSLT 1.0 5.5), not 0.attribute::node()is anode()node test, so its default priority is -0.5 (the same as@*), but it was treated as a name test and given priority 0, which made it wrongly outrank@*. The axis is now stripped before the node test is classified. Closes Apache Xalan conformance casesconflictres29andconflictres30. -
xsl:strip-spaceandxsl:preserve-spacenow match theirelementsname tests by namespace rather than by prefix (XSLT 1.0 3.4). A specifierprefix:*orprefix:localresolves the prefix to its namespace URI at parse time, and a source element is matched on its expanded name in order of specificity (an exact{uri}localover a namespace wildcard{uri}*over*), so a stylesheet that stripsns1:*strips a source element in that namespace whatever prefix the source bound it to, while leaving another namespace untouched. The tests previously compared the raw prefixed name strings, so a differing prefix did not match. Closes Apache Xalan conformance caseswhitespace06andwhitespace07. -
disable-output-escapingis now ignored when anxsl:textorxsl:value-ofproduces the string value of an attribute, comment, or processing-instruction node (XSLT 1.0 16.4: disabling escaping for a value used as something other than a text node is an error, with the recovery being to ignore it). The attribute value built from such content kept its raw-output markers, so the serializer left<and>unescaped in the attribute; the markers are now stripped for these three node kinds (only when the stylesheet uses the feature, so a private-use character in ordinary data is untouched). Closes Apache Xalan conformance caseoutput75. -
The
htmloutput method now percent-escapes non-ASCII and control characters in URI-valued attributes (XSLT 1.0 16.2, HTML 4.01 appendix B.2.1). For the HTML 4 attributes of type %URI (href,src,cite,action, and the rest), each non-ASCII or control/space character in the value is written as the uppercase hex of its UTF-8 bytes (socite="bë.xml"becomescite="b%C3%AB.xml"), while"and&keep their entity form; other attributes are unaffected. Closes Apache Xalan conformance caseoutput32. -
Serializing a processing instruction whose data contains
?>now inserts a space (? >) so the instruction is not terminated early (XSLT 1.0 7.3: a processor recovers from a generated?>by inserting a space after the?). The serializer emitted the?>raw, producing a malformed instruction. Closes Apache Xalan conformance caseoutput72. -
The XSLT conformance harness now decodes source and stylesheet files through the library's own encoding detection (a BOM or the XML declaration) rather than assuming UTF-8, so a non-UTF-8 corpus input is fed to the transform instead of refused. This confirms the library round-trips a UTF-16 source correctly and closes Apache Xalan conformance cases
output80(UTF-16) andattribvaltemplate08, which were unreadable by the harness, not library failures. -
The
htmloutput method now serializes an element whose expanded-name has a non-null namespace URI as XML rather than HTML (XSLT 1.0 16.2). When the result's document element is namespaced, the tree is serialized through the XML path (empty elements self-close, the namespace is declared once and inherited) while the html method still omits the XML declaration; previously such a tree was emitted as HTML, leaving empty namespaced elements unclosed and repeating thexmlnsdeclaration on every element, which is not well-formed XML. A null-namespace tree is unchanged. The rule is applied at document-element granularity rather than per element, so a tree mixing null- and non-null-namespace elements follows its document element's path (the realistic result is all-null or all-namespaced). (Apache Xalanoutput63exercises this rule but remains baselined: its gold differs further only in attribute order, which is not normative.) -
The
htmloutput method now names the document type declarationHTMLrather than the document element name (XSLT 1.0 16.2: "The name following the<!DOCTYPEshould be HTML or html"). Withmethod="html"and adoctype-publicordoctype-system, the emitted declaration is<!DOCTYPE HTML PUBLIC "...">regardless of the result's root element; thexmlmethod still uses the document element name (16.1). Closes Apache Xalan conformance casesoutput40,output48, andoutput60. -
The
htmloutput method no longer escapes a<character in an attribute value (XSLT 1.0 16.2). The HTML output method escapes&and"in attribute values but leaves<and>literal, unlike thexmlmethod; the serializer was escaping<to<there. Thexmloutput method is unchanged (it still escapes both). Closes Apache Xalan conformance casesoutput49andoutput74. -
A string-form
document()reference now resolves against the base URI of the stylesheet element holding it (XSLT 1.0 12.1), which for a declaration in anxsl:included orxsl:imported file is that file's URI rather than the top stylesheet's. The parser already tracked each top-level declaration's source-file base; that base is now carried onto the compiled templates and global variables and threaded into expression evaluation, so a relativedocument('data.xml')(ordocument(''), the self-reference) in an included file resolves against the included file, not the importing one. Closes Apache Xalan conformance casesmdocs12andmdocs13. -
The
document()function now resolves a relative URI reference against the correct base URI (XSLT 1.0 12.1). The two-argument form resolves the first argument against the base URI of the second argument's first node, and the node-set form resolves each reference against the base URI of the node that supplied it; previously every reference resolved against the single global base, so a reference relative to a document thatdocument()itself had loaded from a subdirectory could not be found. Each loaded document now records the URI it came from as its base URI, and a relative reference is resolved against that base before the loader applies its own source-document base. Closes Apache Xalan conformance casesreluri09,reluri10, andreluri11. -
Named templates,
xsl:keynames,xsl:attribute-setnames, andxsl:decimal-formatnames are now matched by expanded QName, like mode and variable names already were. XSLT 1.0 compares all of these by expanded name, so two prefixes bound to the same namespace name the same thing, but they were keyed and compared as raw prefix strings. A declaration name now resolves to{uri}localat parse time, and the runtime references (call-template/use-attribute-setsat parse,key()andformat-number's format name at evaluation) resolve their prefix against the in-scope namespaces the same way. Closes Apache Xalan conformance casesnamedtemplate16,attribset48, andidkey53. -
xsl:copyof the root node now honorsuse-attribute-sets. Copying the root produces no element of its own, so the attribute-set attributes have nowhere to attach on the copy and instead join the enclosing result element (XSLT 1.0 7.5); they were silently dropped. Closes Apache Xalan conformance casesattribset27andattribset29. -
xsl:elementwith an unusable name now passes its content through instead of creating a bad element. XSLT 1.0 7.1.2 makes a name that is not a QName an error; the recovery (pinnedelement-name-not-QName"pass-through") emits the element's content without the wrapper. The check rejected only an empty or multi-colon name, so an undeclared prefix (none:foo) or a name that is not an NCName (this is a string) still produced a malformed element. Closes Apache Xalan conformance casesnamespace40,namespace42,namespace43,namespace60,namespace73, andnamespace79. -
xsl:copy-ofnow carries an element's inherited namespace nodes, not only the ones it declares itself. XSLT 1.0 11.3 copies a node together with its namespace nodes, so copying an element must reproduce its full in-scope set, including a declaration inherited from an ancestor;copy-ofreproduced only the element's ownxmlnsattributes, dropping inherited ones. The inherited declarations are now added to the copied element and the serializer's namespace fixup drops any already in the result scope. Closes Apache Xalan conformance casescopy09,copy27,copy48, andmdocs09. -
xsl:processing-instruction,xsl:comment, andxsl:attributenow use only the text-node content they instantiate, ignoring any element (or other) node together with its content. XSLT 1.0 sections 7.4 and 7.6 (and errata E27 forxsl:attribute) make it an error for the content to create a non-text node, with the recovery being to ignore the offending node and its content; PureXML instead took the string-value of all content, so a copied element leaked its text into the result. Closes Apache Xalan conformance casescopy60,copy56, andcopy58. -
Whitespace adjacent to a CDATA section in a template is no longer stripped. Text and CDATA are one text node in the data model, so
<out> <![CDATA[test]]> </out>is the node" test ", whose surrounding spaces must survive whitespace stripping (XSLT 1.0 3.4). The template body dropped each whitespace-only text node independently, so the spaces around the CDATA vanished. Adjacent text and CDATA children now coalesce into one run, dropped only when the whole run is whitespace; a purely whitespace run between elements is still removed. Closes Apache Xalan conformance casewhitespace13. -
A serialized comment whose text ends with
-or contains--is now made well-formed by inserting a space after the offending hyphen. XML forbids--inside a comment and a-immediately before-->, so a comment ending in-was emitted as<!--text--->(a malformed comment); the serializer now applies the XSLT 1.0 section 16 recovery, sotext-becomestext-. Applied in the XML serializer, the streaming writer, and the HTML serializer (somethod="html"output is covered too); the C14N canonicalizer keeps comments verbatim as its contract requires. Closes Apache Xalan conformance casesoutput89andoutput90. -
A match pattern's predicate now sees the top-level (global) variables. XSLT 1.0 section 5.2 evaluates a pattern predicate with the global variables in scope, but pattern matching ran with no variables bound, so a template like
match="foo[. > $screen]"never matched (the predicate's$screenwas undefined). The transformer now supplies the global bindings to the match cache, which are constant per transform and so do not affect caching. Closes Apache Xalan conformance casesmatch14andidkey43. -
A variable or parameter is now named by expanded QName, not by its literal prefix. XSLT 1.0 section 11.1 names a variable by expanded name, so two prefixes bound to the same namespace name the same variable, but the declared name and the
$prefix:localreference were compared as raw strings, so$new:xdid not see a variable declaredtxt:xeven whennewandtxtshare a namespace. A prefixed declared name now resolves to{uri}localat parse time, and a variable reference falls back to the namespace-resolved name when the raw key is unbound (so a caller-supplied raw binding still works and unprefixed names are unchanged). Closes Apache Xalan conformance casevariable55. -
A template
modeis now matched by expanded name, not by its literal prefix. XSLT 1.0 section 5.7 compares a mode by expanded name, so two prefixes bound to the same namespace name the same mode, but the prefixed QName string was stored and compared verbatim, soapply-templates mode="foo:m"missed a template withmode="moo:m"even whenfooandmooshare a namespace. Mode names onxsl:templateandxsl:apply-templatesnow resolve to{uri}localat parse time. Closes Apache Xalan conformance casemodes16. -
substring-after(s, '')now returnssinstead of the empty string. Per the XPath 1.0 errata, an empty second argument matches at the start of the string, so everything after it is the whole string; the function bailed to empty on an empty separator.substring-before(s, '')stays empty, as the spec requires. Closes Apache Xalan conformance casestring143. -
unparsed-entity-urinow returns the URI of a DTD-declared unparsed (NDATA) entity instead of always the empty string. The function was a stub; it now reads the source document type's unparsed entities, threaded to the function table alongside the document loader. An unknown name still returns empty. Closes Apache Xalan conformance caseexpression02. -
document('')now returns the stylesheet's own document. XSLT 1.0 section 12.1 makes an empty URI reference the document that contains the expression, so a stylesheet can read its ownxsl:*nodes (a common idiom for embedded lookup tables and self-inspection), but the reference was passed straight to the document loader, which has no entry for it, so the call returned an empty node-set. The transformer now keeps its parsed stylesheet tree and thedocumentfunction returns it for an empty reference (caching it like any loaded document; a#fragmentstill applies). Closes Apache Xalan conformance casesselect67,select68,idkey50,mdocs17, andnamespace20. -
An XPath location step now accepts whitespace around the
::axis separator. XPath 1.0 allows whitespace between tokens, sochild :: subis the child axis with node testsub, but the parser only matched::immediately after the axis name, so a spaced separator silently fell back to the child axis with the axis name read as an element test and selected nothing. Closes Apache Xalan conformance casesselect16,select27, andselect28. -
A variable or parameter with empty content and no
selectis now the empty string, not a result tree fragment. XSLT 1.0 section 11.2 makes<xsl:variable name="x"/>equivalent toselect="", soboolean()of it is false, but it was always built as a (non-empty) result-tree-fragment document node, whichboolean()reports as true. Only non-empty content is still an RTF. Closes Apache Xalan conformance casesboolean43andboolean87. -
Top-level (global) variables and parameters now resolve references to one another regardless of declaration order. XSLT 1.0 section 11.4 lets a global reference any other global, but they were evaluated once top-to-bottom, so a forward reference (a variable declared before the one it uses) saw an undefined value and resolved to empty. Globals now evaluate to a fixpoint: each pass re-evaluates the unsettled ones with the latest bindings, so a dependency chain in any document order settles, while a genuine circular reference stops at the pass cap. Closes Apache Xalan conformance cases
variable33,variable34, andvariable35. -
The XPath string-value of a comment or processing-instruction node is now that node's own character data, instead of the empty string. XPath 1.0 sections 5.6 and 5.7 define a comment's string-value as its text and a PI's as its data, so
value-of select="comment()"(and matching, sorting, or comparing on such nodes) returned nothing. The node'sstringValuewas conflated with the text-only concatenation used for an element's string-value, where comments and PIs correctly contribute nothing; that concatenation now uses a separatetextContent, so an element's string-value still excludes comment and PI text while a comment or PI reports its own. Closes 24 Apache Xalan conformance cases across thenode,axes,position,select,modes,conflictres, andvariablecategories. -
A named template (
xsl:call-template) is now selected by import precedence, not document order. When an import and an include both define a template with the same name, the included one carries the including unit's (higher) precedence and must win (XSLT 1.0 section 6), but the lookup took the first match by position, which is the imported template (imports fold in first). The call now picks the highest-precedence definition. Closes Apache Xalan conformance casesnamedtemplate17,namedtemplate18,namedtemplate19, andnamespace13. -
A literal result element brought in through
xsl:includenow keeps the namespace nodes it copies under XSLT 1.0 §7.1.1, instead of losing them in an optimized build. An included stylesheet's declarations are absorbed aftercollectUnitreturns, and each literal result element resolves its 7.1.1 namespace nodes by walking the weak parent chain back to its own (included) stylesheet element. Those elements were retained only in a write-onlyretainedRootsarray, so under optimization ARC released them before the deferred resolution ran and the ancestor chain (thexmlnsdeclarations in scope) vanished, dropping every namespace node the included element should have carried. The result was correct in a debug build and wrong in release. The compose step now pins the retained roots across the resolution withwithExtendedLifetime. Imports were unaffected because they compile eagerly while their tree is still a live local. Catches Apache Xalan conformance caselre/lre05(exclude-result-prefixesscoping across an include), found by running the Apache xalan-test corpus. -
A
redefinewhose group self-reference duplicates a particle across the redefine chain is now rejected as a Unique Particle Attribution (cos-nonambig) violation, instead of being silently accepted. Two gaps combined to miss it. First, the compositional determinism engine treated a<group ref>to the name being redefined (a src-redefine self-reference) as contributing an empty decision set, where instance validation expands it to the ORIGINAL group; it now expands one level to the redefine base exactly as the instance content-model path does, so the original's particles are visible to the overlap test. The expansion only surfaces genuinely reachable items, so the computed decision sets stay a subset of the true model and the change can catch a real ambiguity but never invent one. Second, the UPA check walked only the root document, so an ambiguity created by a rootredefinein a complex type that lives in an INCLUDED document was never evaluated; it now runs over every schema root in the composition under the shared (redefine-effective) context, mirroring the existing cross-document top-level-declaration check. Closes XSTSschN10(a three-level redefine chain). XSTS invalid-schemas-accepted 11 -> 10, valid-schemas-rejected held at 0. -
A
complexContentrestriction whose base complex type has SIMPLE content is now rejected (cos-ct-restricts 5.2). AcomplexContentrestriction's {content type} is element-only, mixed, or empty (derived from its model group), which can never validly restrict a simple content type, so restricting a simple-content base this way is invalid. Only a.simplebase was flagged before; a complex type withsimpleContentslipped through. The check is scoped to the restriction direction: the extension direction has a valid attribute-only idiom and stays handled by the existing simpleContent-extension rule. Catches particlesZ039. XSTS invalid-schemas-accepted 16 → 15, valid-schemas-rejected held at 0. -
A content-bearing derived group is no longer accepted as a restriction of a base single element. XSD 1.0 §3.9.6 "Particle Valid (Restriction)" defines a case for an element restricting a base group (RecurseAsIfGroup) but NONE for the reverse, so a derived
sequence/choice/allthat can contribute a child never validly restricts a single base element, regardless of how its leaves are named or counted. The oracle had a deliberate non-spec "mirror" extension that bounded such a derived group by the element's effective occurrence range and accepted it; that masked a real violation inside achoice-to-choicerestriction, where a derivedsequencebranch was allowed to map onto a base element branch it cannot legally restrict. The case now returns invalid (a content-FREE derived group is still a valid restriction of any emptiable base, handled separately). Catches particlesHb011. XSTS invalid-schemas-accepted 17 → 16, valid-schemas-rejected held at 0. -
A defaulted or fixed
xs:IDREF/xs:IDREFSattribute now participates in document-scoped ID/IDREF resolution (cvc-id). When the instance omits the attribute itsdefault/fixedvalue applies, so a defaulted IDREF must still resolve to a matchingxs:ID; the reference was recorded only for attributes physically present, so a dangling defaulted IDREF was wrongly accepted. The supplied value is now recorded for ID/IDREF checking exactly as a present value would be (shared by the tree and streaming validators, which both callvalidateAttributes). A required absent attribute is still reported as missing, and a defaulted IDREF that does resolve to an ID stays valid. Catches idZ012. XSTS invalid-instances-accepted 20 → 19, valid-instances-rejected held at 0. -
An
xs:allgroup withminOccurs="0"now requires its members once the group is present in the instance. The group'sminOccurs="0"makes the GROUP optional (it occurs zero or one times), not each member optional: an absent group (no matching children) is valid, but a present group (any matching child) must still satisfy every member's ownminOccurs. The parser previously forced each member'sminOccursto 0 when the all-group was optional, so a non-empty but incomplete optional all-group (a required member missing) was wrongly accepted. Members now keep their declared occurrence, and the absent-group case is handled at validation time by skipping the required-member check only when the all-group contributes no children at all; a required all-group (minOccurs="1") is unchanged. The fix is applied identically on the tree and streaming paths (both callallStructureErrors). Because members now keep their trueminOccurs, the particle-restriction check (cos-group-restrict) also sees them correctly: a complexContent restriction that relaxes a required member of an optional all-group base is now rejected too, so this catches mgZ001 (instance) and particlesK006 (schema). XSTS invalid-instances-accepted 21 → 20 and invalid-schemas-accepted 18 → 17, valid-instances-rejected and valid-schemas-rejected both held at 0. -
A nilled element (an
xsi:nil="true"on a nillable declaration) whose declaration carries a fixed{value constraint}is now rejected during instance validation (cvc-elt.3.2.2). A nillable element MAY declare a fixed value (the schema is valid); the conflict arises only when an instance actually nils such an element, since a nilled element has no value and so cannot take its fixed one. The check rides the existingnilErrorschokepoint (shared by the tree and streaming validators), using the matched particle's constraint with the by-local-name element-constraint map as a fallback, so it is enforced identically on both paths; a nillable element that supplies its fixed value, and a nilled element with no fixed value constraint, are both unaffected. Catches addB065. XSTS invalid-instances-accepted 22 → 21, valid-instances-rejected held at 0. -
A top-level (global) element or attribute declaration carrying a
formattribute is now rejected even when it lives in an INCLUDED or IMPORTED document. Theformattribute is permitted only on a local declaration (XSD 1.0 schema-for-schemas:topLevelAttribute/topLevelElementhave noform, since a global component's qualification is fixed); the check (topLevelDeclarationFindings, which also enforces a global needing anameand forbids the other local-onlyuse/ref/minOccurs/maxOccurs) already existed but ran only over the document being compiled, so a violation in anxs:include/xs:import/xs:redefinetarget was missed and the composed schema wrongly accepted. The same global-component check now runs over the other schema-document roots in the composition (the main document is excluded as already covered, andredefineentries are skipped because each schema root walks its ownredefinechildren). The constraint is absolute and document-position-independent, so no valid schema is newly rejected; libxml2 rejects both cases for the same reason. Catches attQ016 (import) and attQ018 (include). XSTS invalid-schemas-accepted 20 → 18, valid-schemas-rejected held at 0. -
An identity-constraint field whose DECLARED target is a complex type with non-simple content is now rejected even when the instance node has no element children. The earlier fix rejected a field whose selected node carries element children (structural); this adds the case where the field's declared type is an empty or element-only/mixed complex type (an attributes-only element resolves to an empty-content complex type, so its instance node is childless yet not simple-typed). The field's target type is resolved through the schema type model (the selector target's type, then the field's same-named child element); a simple or simpleContent target, a type reached only through a named reference, a complex field path, or an unresolvable target is left alone, so no valid field is rejected. Catches idG006. XSTS invalid-instances-accepted 23 → 22, valid-instances-rejected held at 0.
-
A substitution-group
blockon a head element now bars substituting a member whose INLINE (anonymous) type reaches the head's type by a blocked method. The substitution-group filter already dropped a member when the head's{disallowed substitutions}(or the head type's{prohibited substitutions}) blocked the member's derivation method, but only for members with a NAMED type recorded in the derivation backbone; an element with an inline complex type had no backbone entry, so a member extending the head's type was wrongly kept even when the head blockedextension. Inline-typed elements now record their derivation under a synthetic key (an element key plus a#marker no QName can contain, so it cannot collide with a real type and is unreachable as any other derivation base), so the filter sees the inline member's method. A member reaching the head's type by an UNblocked method, a plain inline type with no derivation, and same-local-name elements in different namespaces are all unaffected. Catches disallowedSubst00105m (and a sibling). XSTS invalid-instances-accepted 25 → 23, valid-instances-rejected held at 0. -
An identity-constraint (
key/unique/keyref) field that identifies a node with complex content is now rejected during instance validation. Per cvc-identity-constraint.3 / c-fields-xpaths a field must evaluate to a node with a simple type (a simple-typed element, an element with simple content, or an attribute); the cardinality clause (at most one node) was enforced, but the simple-content clause was not. The check is structural and false-positive-free: a field whose selected node is an element carrying element children has complex (element-only or mixed) content and can never be simple-typed, so it is flagged, while a simple-typed or simple-content element (text only), an empty or text-only node, and an attribute are left alone. Catches idK012. XSTS invalid-instances-accepted 26 → 25, valid-instances-rejected held at 0. -
The attribute-use uniqueness check (
ag-props-correct.2/ct-props-correct.4) now flattens a redefined attribute group's self-reference and excludes prohibited uses. When an attribute group flattens its uses through<attributeGroup ref>references, a reference to a redefined group whose redefinition self-references its former self previously stopped at the cycle guard, so the original's attributes were never reached and a clash they cause was missed. The self-reference now expands to the group's pre-redefine base definition (already retained for this purpose), so the original's attributes join the set; a genuine non-redefine cycle still terminates without contributing. Separately, ause="prohibited"attribute is now excluded from the duplicate-name and single-ID counts, since a prohibited use is a removal marker, not a member of{attribute uses}(XSD §3.4.2/§3.6.2); this also keeps a self-reference that re-imports an attribute and then prohibits it from reading as a duplicate. Catches attQ011 and attQ017 (an attribute group referencing both a global attribute and a redefined self-referencing group that supplies the same-named attribute). XSTS invalid-schemas-accepted 22 → 20, valid-schemas-rejected held at 0. -
A redefinition of an
attributeGroupthat SELF-references the original and also directly re-declares one of its attributes is now rejected. The self-reference re-imports all of the original's attribute uses, so a same-named direct re-declaration produces two attribute uses of the same name, a duplicate forbidden by ag-props-correct.2. The check fires only for a re-declared, non-prohibited attribute the original directly declares, and only when both the original and the re-declaration land in no namespace (a qualified attribute's target namespace depends on cross-documentform/chameleon resolution, so it is not compared, staying lenient); a self-referencing redefinition that adds genuinely new attributes (attgC007/attgC038/groupB018) stays valid. Catches schM8 (c-g2self-references and re-declaresa1). XSTS invalid-schemas-accepted 23 → 22 (schM8), valid-schemas-rejected held at 0. -
A redefinition of an
attributeGroupthat WIDENS a re-declared attribute's type is now rejected. A redefinition is a restriction (src-redefine / cos-ct-restricts), so a re-declared attribute's type must derive by restriction from the original's. The check is built-in-only and therefore certain: it fires only when both the original and the re-declared attribute name a built-in type (resolved to the XSD namespace) and the re-declared built-in does not derive from the original's in the XSD derivation lattice (intdoes not derive fromboolean). A user, inline, or absent type on either side is not compared (declines, staying lenient): an absent original type isanySimpleType, which any type validly restricts, a user/inline type's derivation is not resolvable here, and a user type whose local name coincides with a built-in (a{urn:x}int) resolves to a different namespace and is not mistaken for the built-in, so no valid restriction is rejected. Catches schM4 (b1re-typedboolean→int). XSTS invalid-schemas-accepted 24 → 23 (schM4), valid-schemas-rejected held at 0. -
A redefinition of an
attributeGroupthat adds an attribute the original lacks through a NON-selfattributeGroupreference is now rejected. A redefinition is a restriction of the original (src-redefine / cos-ct-restricts), so it may not introduce an attribute the original does not declare unless the original carries an attribute wildcard. The add-attribute check previously resolved only the redefinition's DIRECT declarations and was suppressed whenever the redefinition carried anyattributeGroupreference, which conflated a self-reference (re-importing the original's own attributes, valid) with a non-self reference (injecting foreign attributes, invalid). The redefinition's contributed attribute names are now resolved through its non-self references and the suppression keys on a genuine self-reference (matched by namespace and local name); resolution declines, staying lenient, whenever the contributed set is not fully knowable (an unresolved group, an attribute-by-ref, ananyAttributewildcard, or a further nested reference). This catches attgC028 (a redefinition whoseref="car"replaces the original's attributes with foreign ones) while keeping attgC007/attgC038/groupB018 (added attributes alongside a self-reference) valid. XSTS invalid-schemas-accepted 25 → 24 (attgC028), valid-schemas-rejected held at 0. -
An unprefixed
type/base/itemType/memberTypesreference to a type declared in a non-empty target namespace is now rejected as undeclared. With no defaultxmlns, an unprefixed QName resolves to the absent namespace, sotype="t"is{}t, not{urn:x}twhere the type lives;isUndeclaredReferenceTypematched the type by local name and accepted it. It now mirrorsisUndeclaredReferenceRef: a no-namespace reference is undeclared when the schema has a non-empty target namespace (a chameleon-included global, and an imported no-namespace type via the foreign pools, remain accepted). XSTS invalid-schemas-accepted 27 → 25 (addB009/test66059 and one more), valid-schemas-rejected held at 0. -
A schema reference (
ref/substitutionGroup) that resolves to the XSD namespace is now rejected as undeclared. The XSD namespace declares no referenceable user component, soisUndeclaredReferenceTypealready rejected atype/basein it, butisUndeclaredReferenceReffell through to a lenient accept. This is reached when an unprefixed reference picks up a defaultxmlnsof the XSD namespace, so it is not in the target namespace where the schema's own components live: e.g.xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="foo"with<element ref="root"/>resolves to{XSD}root, not{foo}root, and is undeclared. A schema that itself targets the XSD namespace, and a genuinely loaded XSD-namespace document, are unaffected. XSTS invalid-schemas-accepted 29 → 27 (xsd013/xsd014), valid-schemas-rejected held at 0. -
Unique Particle Attribution now checks a
complexContentextension against its EFFECTIVE content model (the base type's content followed by the extension's), not just the extension's own particles. A base whose trailing particle can recur or end optionally competes with the extension's leading particles, so an element could match either across the extension boundary; that cross-boundary non-determinism was missed and the schema wrongly accepted. The base content is resolved and prepended only for a local (target-namespace), non-cyclic base chain; a foreign, built-in (xs:anyType), unresolved, or cyclic base leaves the check on the extension's own content, so no valid schema is newly rejected. XSTS invalid-schemas-accepted 31 → 29 (particlesZ022 and one more), valid-schemas-rejected held at 0. -
Unique Particle Attribution now detects overlapping
xs:anywildcards involving##other.wildcardsOverlapreturned false for any pair involving##other, so two competing wildcards whose namespaces actually intersect, two##otherconstraints (which always share infinitely many namespaces) or##otheragainst an enumerated set naming a non-target, non-absent namespace, were not flagged as non-deterministic, and the schema was wrongly accepted. The overlap is now computed correctly:##other∩##otheralways overlaps, and##other∩enumerated overlaps iff the set names a namespace that is neither absent (##local) nor the target. Disjoint cases (##otheragainst##localor against the target namespace) remain deterministic, so no valid schema is newly rejected. XSTS invalid-schemas-accepted 35 → 31 (wildI009/wildI013/wildI014 and one more), valid-schemas-rejected held at 0. -
The content-model matcher no longer blows up on a nested model group with a huge but finite
maxOccurs(#192, partial). The counted-automaton active set is bounded by the product of the per-counter saturation caps; a finitemaximumthe input cannot reach (maximum > inputLength + 1) is now saturated at its minimum, exactly as an unbounded counter is, because the value can never reach that maximum under any continuation (the cap is at mostinputLength + 1), sobelowMaximumalways holds and every value at or above the minimum is matching-equivalent. A document that did not terminate within a minute against such a model (the Z035 worst case) now validates in linear time, with no change to any accept/reject decision (full XSTS suite still 0/35/0/26; an adversarial no-caps reference-matcher differential confirms no verdict changed, including nullable bodies and the occurrence boundary). Guarded byContentMatcherWorstCaseTests. A model with several moderate (input-reachable)maxOccursnested deeply still enumerates a large active set and is tracked under #192. -
A reference to a model group whose content is an
allgroup must havemaxOccursof 1 andminOccursof 0 or 1 (XSD 1.0 cos-all-limited, #158). A<group ref>that resolves to an<xs:all>group withmaxOccurs="2"is now rejected (corpus particlesEa025); the direct<all>element was already checked. XSTS invalid-schemas-accepted 36 -> 35, valid-schemas-rejected held at 0. -
A complexContent restriction that narrows an element to a type derived from the base element's type by extension is now rejected (XSD 1.0 cos-particle-restrict NameAndTypeOK.2.2, #158): the element-type-derivation check follows the derivation chain across
restrictionsteps only, so an extension-derived type is not a valid restriction (corpus particlesIj008). Built-in lattice and union-base derivation are unchanged. XSTS invalid-schemas-accepted 37 -> 36, valid-schemas-rejected held at 0. -
A
fixedvalue on an element whose type is a complex type withsimpleContentis now compared in the simple type's value space (cvc-elt.5.2.2.1), not as a raw string (#202), so05satisfies anxs:intfixed="5". The complex-element fixed check on both the tree and streaming paths now passes thesimpleContenttype as the value type, in lockstep, as the simple-typed-element path already does. -
The streaming XSD validator now applies an element's
default/fixedvalue constraint to its simple content (#200): an empty element takes its constraint value, and that value (not the empty string) is validated against the type (cvc-elt.5.1.2 / cvc-elt.5.2.2.2). The constraint comes from the matched particle (keyed by namespaced identity), not the flat by-local-name map, so two same-local- name elements in different types do not collide. Both paths share oneeffectiveSimpleText(particleConstraint:)helper, so an empty element with a valid default is no longer falsely rejected and streaming agrees with the tree path. -
The streaming XSD validator now agrees with the tree validator on
xsi:typeand type resolution (#186). Type resolution is keyed by namespaced identity ({ns}local) through in-scope prefix bindings accumulated down the element stack: thexsi:typeoverride type, the undeclared-type check, and root/child element declarations (so two globals or types sharing a local name in different namespaces no longer collide; root resolution mirrors the tree path, with the bare local-name map used only for a no-namespace root). The streaming path now runs the full cvc-elt.4 gate the tree path runs (a blocked, abstract, not-validly-derived, list/union, or anyType-for-anySimpleType override, and an abstract declared type carrying noxsi:type, are rejected identically). And it resolves a type from an element's ownxsi:typewhere there is no governing declaration: an undeclared root (the W3C Sun target-namespace fallback) and a strict-wildcard child, which the streaming path previously rejected as undeclared. -
An attribute declaration may no longer land in the XSI namespace (XSD 1.0 Schema Component Constraint "xsi: Not Allowed", §3.2.6). A schema may target the XSI namespace and declare unqualified attributes (they land in no namespace), but a top-level attribute, or a local one made qualified by
form="qualified"orattributeFormDefault="qualified", lands in XSI and is now rejected (corpus attKb018a, attKa015) while unqualified ones stay valid (attKb018, attKc018). XSTS invalid-schemas-accepted 39 -> 37, valid-schemas-rejected held at 0, no other bucket moved. -
A prefixed reference (
type/base/itemType/ref/substitutionGroup/memberTypes) whose namespace is never imported is now rejected on the cross-document skip path (XSDsrc-resolve). When a schema declares imports but loads no external document, the pool-based check stands down; a reference into a namespace that is neither the target, the XSD or XML namespace, nor named by anyxs:importcan resolve to no component and is flagged. To stay sound when the import closure is unknown, the check stands down whenever any external carries aschemaLocation(an unloaded located document may transitively import the referenced namespace), applying only when every external is location-less. XSTS invalid-schemas-accepted 43 -> 39 (schZ011_a-d), valid-schemas-rejected held at 0, no other bucket moved. -
A non-nillable element may now carry no
xsi:nilattribute at all, whatever its value (cvc-elt.3.1). Previously onlyxsi:nil="true"on a non-nillable element was rejected;xsi:nil="false"slipped through.ComplexValidator.nilErrorsnow flags the presence of anyxsi:nilon a non-nillable element, while a nillable element still acceptsxsi:nil="false"(explicitly not nilled). XSTS invalid-instances-accepted 28 -> 26, no other bucket moved (valid-rejected held at 0 across all 14383 groups). -
An element with neither a
typeattribute nor an inline type, but asubstitutionGroup, now takes the type definition of its head (XSD 1.0 3.3.2) instead of defaulting toanyType.XSDParser.elementTyperesolves the head declaration (transitively, guarding asubstitutionGroupcycle) and uses its type, so an instance of such a member is validated against the head's type. XSTS invalid-instances-accepted 29 -> 28 (typeDef00204m), no other bucket moved (valid-rejected held at 0 across all 14383 groups). -
xsi:typenaminganyTypeis now rejected on an element whose declared type isanySimpleType(cvc-elt.4.3.2.1).anyTypeis the ur-supertype of every type, so it is never validly derived fromanySimpleType(its own subtype). The general not-derived check stays silent on ur-type declared types to avoid false positives; this adds only the provably-invalidanyType-for-anySimpleTypepairing, leavinganyTypevalid on ananyType-declared element. XSTS invalid-instances-accepted 30 -> 29 (stZ056), no other bucket moved. -
dateTime,date,gYearMonth, andgYearnow reject the year0000. XSD 1.0 has no year zero (the year before0001is-0001), so0000-01-01T00:00:00and the other0000forms are not valid values.DateTimeParser.parseYearOnlyrejects a zero magnitude for both signs, so-0001and other negative years still parse. XSTS invalid-instances-accepted 31 -> 30 (dateTime011), no other bucket moved (valid-rejected held at 0). -
Fixed a dead
rootLocationfallback inXSDParser.containerLocationMap. The map value type is itselfString?, soloc ?? rootLocationbound the coalescing at the outer optional and never usedrootLocation(the compiler flagged it), leaving the compile root mapped to nil even when a circular import had loaded it under aschemaLocation. An explicitString?annotation keeps the coalescing at theStringlevel so the root inherits its location. Conformance is unchanged (XSTS still settles at 0/43/0/31); theSchemaNameUniquenessroot-index dedup case is retained, not removed, because removing it on the theory it was now redundant regressed three valid schemas to rejected, so it does real work beyond the fixed fallback. -
Bounded the content-model matcher's active-configuration set per counter (
ContentNFA/ContentMatcher). Each occurrence counter now saturates atmin(maximum, inputLength + 1), and an unbounded counter at itsminimum(every higher count is matching-equivalent, sincecanExitonly tests the minimum andbelowMaximumis vacuous), instead of every counter sharing oneinputLength + 1ceiling. Validating a document with a long run of one element against a content model with nested occurrence bounds (e.g.sequence{1,1000}(e1{1,unbounded})over ~1000 elements) no longer expands the epsilon-closure towardinputLengthraised to the counter count: the XSTSparticlesQ005instance went from a multi-minute near-hang to about two seconds, with identical validation results (the full XSTS suite still settles at 0/43/0/31, and the suite, which previously did not terminate, now completes). -
Silenced an optional-interpolation warning in the union dedup signature (
XSDParserUnion): thelocalNameinterpolation now guards with?? ""to match thename-attribute interpolation on the same line. The signature is an internal dedup key compared only against same-run signatures, so the change is behavior-preserving (it removes theOptional("…")rendering, not any key collisions). -
Documentation accuracy sweep: brought stale current-state figures into line with the code and the enforced gate. The README test count (986/169 -> 1448/258) and XSTS status quad, the roadmap gates table, and the Antigravity onboarding baselines now read the enforced
0 / 43 / 0 / 31; the encoding coverage (the README feature list and theParsing.InputEncodingdoc comment) now lists the full shipped set (the ISO-8859 family, Windows code pages 1250-1258, KOI8-R/U, and the CJK multi-byte encodings) instead of a three-entry subset; the "remaining parity" section now states that libxml2 documented-surface parity is complete (epics #71/#74/#105 closed) with production readiness (#167) as the remaining work; the READMEcheck-all.shexpansion lists the validation-coverage, validation-field, and changelog gates; and the SECURITY.md reporting address is corrected. -
Replaced XSD content-model occurrence unrolling with a counted automaton.
ContentMatchernow tracks occurrence counters in active configurations instead of copying particle bodies into states, preserving exact finite{m,n}behavior for high numeric bounds and removing the instance-time silent widening at the old state ceilings. XSD occurrence attributes are preserved as decimal magnitudes beyondIntrange somaxOccurs="100000000000000000000"no longer falls back to the default1. -
XSTS invalid-schemas-accepted (#145): global element references now carry the referenced declaration's type name, fixed/default constraint, nillable flag, and
blockmetadata into Particle-Valid-Restriction. A local element with an implicitxs:anyTypetherefore no longer satisfies NameAndTypeOK against a same-named global ref typed asxs:decimal, closing elemZ028e while preserving a valid narrowing toxs:short. invalid-schemas-accepted 44 -> 43, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): substitution-group closure now stops chaining through an intermediate member whose own
blockcontainssubstitution. The blocked element itself may still substitute for its direct ancestor, but its descendants are no longer added to higher ancestor heads, so elemZ027_c (a restriction througha -> b(block=substitution) -> c -> d) is rejected while the valid sibling where the blocked member itself is used remains accepted. invalid-schemas-accepted 45 -> 44, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a substitution-group head typed
xs:anySimpleTypenow admits simple type members and complex types withsimpleContent, but rejects element-only, empty, and mixed complex-type members. The shared derivation oracle treatsanySimpleTypeas a broad ur-type for other restriction checks, so the substitution-group rule now adds its own value-space guard before accepting the affiliation. stZ048 is rejected while the valid simple/simpleContent near-misses remain accepted. invalid-schemas-accepted 46 → 45, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
complexContentextension that explicitly changes mixedness, or that adds element content while defaulting tomixed="false", must match the base type's mixed setting. This rejects ctZ010d (mixed base extended with new element content by an omitted/default-false derived type) while preserving the valid ctZ012b near-miss (empty extension inherits the mixed base) and the invalid ctF008 explicit mixed mismatch. invalid-schemas-accepted 47 → 46, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
simpleContentrestriction that supplies an inlinesimpleTypemust use a type that is itself a valid restriction of the base complex type's simple content. A list ofxs:intis not a restriction of an atomicxs:decimalcontent type, so particlesZ018 is now rejected. Valid inline atomic restrictions become the effective simpleContent type for instance validation, including facets declared beside the inline type. invalid-schemas-accepted 48 → 47, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a constraining facet may not be applied where a
simpleContentrestriction's base chain resolves toxs:anySimpleType, the ur simple type, which carries no constraining facets (XSD Part 2 §3.2.1); stZ010 (a complex type extendingxs:anySimpleType, restricted by another that addsminLength) is now rejected. The base chain is followed through local complex types' ownsimpleContentbases; a base that bottoms at any other built-in, a simple type, or an external/unresolved type is left alone, and a restriction supplying its own nestedsimpleTypebase is skipped (the facet applies to that), so a facet on a string-, int-, or other real-typed simpleContent stays accepted. invalid-schemas-accepted 49 → 48, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved. -
XSTS invalid-schemas-accepted (#145, #161): a redefinition of an attribute group may not ADD an attribute the original does not declare (a superset is not a restriction, src-redefine / cos-ct-restricts); schM3 (redeclaring the original's attributes plus a new
b4) is now rejected. The base is resolved by namespaced identity with a{}namechameleon fallback (a no-targetNamespace redefined schema is absorbed into the redefining namespace). To stay false-positive-free the added-attribute check runs only when the original's attribute set is fully known from its direct declarations (the original has no nestedattributeGroupreference) AND the redefinition itself carries noattributeGroupreference: a self-reference brings in the original's attributes, so declaring further ones alongside it is a valid redefine (W3C attgC007/attgC038/groupB018, and test75083/xsd003b), not an addition. invalid-schemas-accepted 51 → 49, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0; five self-reference over-rejections surfaced by the gate were fixed before commit), no other bucket moved. -
XSTS invalid-schemas-accepted (#145, #161): a non-self-referencing model group redefined inside
xs:redefinemust be a valid RESTRICTION of the group it redefines (cos-group-restrict/src-redefine6.1.2), so its content model may not widen the original's language. The check reuses the same particle-restriction oracle as complex-type restriction, resolving the original group by namespaced identity (with a chameleon{}namefallback for a no-targetNamespace redefined schema). Cases now rejected include adding required content to an empty base (schZ006), dropping a requiredallmember so documents without it become valid (schL1), reordering anall(schL5/L6, since the spec's Recurse rule is order-preserving even forall), and growing a group to a superset (schL8), plus the multi-document and chained redefines schN4/5/6/7 and schL10. To stay false-positive-free the check skips a redefinition carrying a self-reference (governed by the self-reference well-formedness rule and not re-parseable here) and declines to judge a base containing amaxOccurs=0"pointless particle" (the contested normalization case where W3C accepts a redefinition re-enabling that member, mgO013); a genuine restriction (dropping an optional member, an unchanged redefinition) is accepted. invalid-schemas-accepted 64 → 51, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0; an over-rejection of mgO013 surfaced by the gate was fixed before commit), no other bucket moved. Disclosed under-rejections: self-referencing redefinitions (schN10) andmaxOccurs=0-base redefines. -
XSTS invalid-schemas-accepted (#145, #161): a redefinition of an attribute group restricts the original, so it may neither eliminate a required attribute the original declares, nor re-introduce a prohibited one as usable, nor relax a fixed value the original sets (src-redefine / cos-ct-restricts). attgZ002 (dropping the required
att1), attgZ003 (re-adding the prohibitedatt1as optional), and schM10 (changing afixedattribute to adefaultone) are now rejected. Re-declaring a prohibited attribute AS prohibited keeps it prohibited and stays valid, the drop-required check is skipped when the redefinition uses a nested attributeGroup reference (which could re-introduce the attribute), and the fixed-value check fires only when the redefinition declares the matching attribute as non-prohibited with a differing fixed value (keeping the same fixed value, prohibiting it, or omitting it is left alone), so no valid redefinition is rejected. invalid-schemas-accepted 67 → 64, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved. -
A complex type whose
simpleContentderives from a list or union simple type now keeps that variety, so its element content is validated as a list or union rather than collapsing to an atomic value space. PreviouslysimpleContentTypedropped the base's variety, so an invalid item in a list-typed simpleContent (e.g. a non-int in a list-of-int) was accepted. A valid list/union value is unaffected. (Under-rejection fix surfaced by the libxml2 differential; the W3C suite does not exercise it, so the conformance counts are unchanged.) -
XSTS invalid-schemas-accepted (#145): a substitution-group member whose type is a list or union derives only from
anySimpleType, so it may affiliate only to a head typedanySimpleType(or to the same type, or as a recorded restriction of the head's type). The substitution-type check skipped derivation entirely when either the member or head type was a list/union; a list/union-typed member affiliated to an unrelated head type (addB141: a union member of a list-typed head) is now rejected, while a list/union member of ananySimpleTypehead and a restriction of a list head remain valid. invalid-schemas-accepted 69 → 67, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#147): a
blockon a LOCAL element declaration is now keyed by the element's instance (qualified) name in the derivation backbone, so an xsi:type substitution blocked on an unqualified local element is barred. The namespaced backbone keyed every element by the target namespace, but a local element is in the target namespace only when qualified (its ownform, else the schema's elementFormDefault); an unqualified local element's block went unfound (typeDef00801). invalid-instances-accepted 32 → 31, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#147): an instance
xsi:typemay not name an abstract type. An abstract type cannot be the type of an instance element (cvc-elt.4.3.2), so it is invalid as a substitution even when the declared type would otherwise admit it (typeDef01101); a concrete type is unaffected. This complements the existing check for an abstract DECLARED type with noxsi:type. invalid-instances-accepted 34 → 32, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#147): an element with a
fixedvalue constraint and a mixed content type must carry its fixed character value with no element children (cvc-elt.5.2.2.2.1). The fixed check validated only the concatenated text, so a mixedfixedelement whose text matched but which also contained child elements (ctZ013) was accepted; element children are now rejected. An empty element (which takes the fixed value) and text equal to the fixed value remain valid. invalid-instances-accepted 37 → 34, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#147): a
simpleContentderivation whose base is another complex type with simpleContent now resolves through the chain to the underlying simple type, so the element's text is validated against it. A type restricting or extending a complex type that ultimately derives from a built-in (e.g.TestrestrictsTest2which extendsxs:int) validates its content as that built-in rather than as unconstrained string;baseTD00101's non-int text is now rejected. The base resolution recurses throughcomplexTypeNodes, accumulating facets, with a cycle guard. invalid-instances-accepted 39 → 37, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#147):
use="prohibited"on an attribute now excludes it from the complex type's effective{attribute uses}(XSD 1.0 §3.4.2) during instance validation, so a prohibited attribute is undeclared: its presence is rejected when no attribute wildcard admits it (attJ), and still admitted (and validated per the wildcard'sprocessContents) when one does (attZ002). The prohibited declaration is kept in the type rather than dropped, so schema-validity checks still see it (the invalid-schemas-accepted count is unchanged). invalid-instances-accepted 43 → 39, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#147): an element may now carry at most one attribute whose type is
xs:ID(XSD 1.0 §3.4.6). Two ID-typed attributes admitted through anxs:anyAttributewildcard (the schema itself stays valid, as a type with two declared ID attribute uses is already a compile error) are rejected at instance time. The count covers both a matched declared use's type and the global declaration a matching wildcard admits, including a restriction ofxs:ID;xs:IDREF/xs:ENTITYand a lone ID attribute are unaffected. invalid-instances-accepted 45 → 43, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#146, #147): an identity-constraint (
unique/key/keyref) field that selects an absent attribute or an empty element whose declaration carries adefault/fixednow takes that value as its identity-tuple component, so two members can collide on the defaulted value (idG011/idG012 default, idF016/idF017 fixed). The schema'sdefault/fixedvalue constraints on identity-field targets are gathered at compile time, keyed the same way as the field types, and threaded through to the identity validator. The substitution is exact about absent versus present-empty: an@attrfield is defaulted only when the attribute is ABSENT (empty node-set), never when it is present with value""; a.element field is defaulted only when the selected element has no character content. Applying a default only adds an identity value where the schema declares one, so it can create a genuine duplicate-key detection but never reject a valid instance. invalid-instances-accepted 50 → 45, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#147): a
blockon a substitution-group head's TYPE, not only on the head element, now bars a member from substituting. The substitution-group filter drops a member whose type reaches the head's type by a blocked derivation method, using the union of the head element's{disallowed substitutions}and the head type's{prohibited substitutions}; previously only the head element's ownblockwas consulted, so ablock="restriction"on the head's complex type let a restriction-derived member substitute. The element-levelblock="substitution"outright bar is unchanged, and a member derived by a method the block does not list still substitutes. invalid-instances-accepted 53 → 50, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#181, #147): the
xsi:typenot-derived check now also flags a complex substitute that records no base of its own. Such a type derives only fromanyType, so it cannot validly substitute for a non-ur complex declared type; naming the declared type's own complex ancestor, or an unrelated baseless complex type, is rejected. A complex substitute is in scope even when absent from the derivation backbone (its non-derivation from a non-ur type is certain), while an atomic substitute must still be in the backbone so a built-in stays silent. A genuine extension/restriction substitute is accepted, and a baseless complex substitute under ananyTypedeclaration is admitted. invalid-instances-accepted 60 → 53, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#181, #147): the
xsi:typenot-derived check (cvc-elt.4.3.2.1) now covers ATOMIC simple substitutes, not only complex ones. Anxsi:typenaming an atomic type that is not derived from the element's declared type, such as a sibling restriction or the declared type's own ancestor, is rejected. The check still fires only when both the substitute and the declared type are recorded in the namespaced derivation backbone with no path between them, so a built-in substitute or declared type (absent from the backbone) and the ur-types stay silent (under-reject), avoiding any built-in-derivation false positive; a genuinely derived restriction substitute is accepted. invalid-instances-accepted 66 → 60, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#181, #147): an
xsi:typenaming a list or union simple type is now rejected unless the element's declared type is a ur-type (anySimpleType/anyType). A list or union type derives only fromanySimpleType(XSD Part 2), so it cannot validly substitute for an element of any more specific atomic or complex type (cvc-elt.4.3.2.1); naming one on, say, anxs:int-declared element is invalid. A genuinely derived (restriction) substitute, a list/union under ananySimpleTypedeclaration, and a declared type that is itself a list or union are all left alone, so no valid instance is newly rejected. The declared type is resolved through the element-ref / type-reference chain, so the check also fires for an element reached via<xs:element ref>whose global declaration is atomic. This widening became safe once the derivation backbone was keyed by namespaced identity (#186); the conservative atomic/built-in not-derived cases remain under-rejected pending the full type-identity relation. invalid-instances-accepted 81 → 66, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#186, #147): the instance-validity derivation and substitution tables are now keyed by namespaced identity (
{ns}local) instead of bare local name, so two same-local-name types or elements in different namespaces no longer collide.DerivationTableskeeps its bare-keyed fields for the schema-validity machinery (Particle-Valid-Restriction and the consistency rules) and gains parallel namespaced fields built in the same pass; only the instance consumers (thexsi:typeblocked/not-derived/abstract checks, the substitution-group closure, andreferenceParticle) read the namespaced tables, reducing atype:{ns}localreference, a bare name, or an element ref to a{ns}localkey. A key miss stays permissive (no constraint), so the re-keying cannot over-reject; the single-document corpus behaviour is unchanged. With the namespaced backbone in place, a schema'sblockDefaultnow supplies the{disallowed substitutions}(block) of every element and complex type that states none (ownblockoverrides;block=""clears; intersected with the methods each component admits), without one namespace's defaults shadowing another's. invalid-instances-accepted 89 → 81, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0; an adversarial review exercised same- and cross-namespace substitutions, no-targetNamespace keys, transitive substitution groups, and blockDefault override cases), no other bucket moved. -
XSTS invalid-instances-accepted (#147): a complex type's effective attribute wildcard is now the INTERSECTION of the
anyAttributewildcards it draws from its own declaration and each referenced attribute group (XSD 1.0cos-aw-intersect), not their union. An attribute is admitted only if every source admits it, so two attribute groups constraining disjoint namespaces (e.g.urn:aandurn:b) admit nothing and an attribute in either is rejected. A type with a singleanyAttributesource is unchanged (intersection with no other source is the lone wildcard), and a complexContent extension's union with its base wildcard is unaffected. NewWildcard.intersectioncovers the namespace cases (##anyidentity, enumerated set intersection, enumerated-with-##otherexclusion) and keeps the less restrictive result for the inexpressible##other/##otherand mismatchedprocessContentscases, so the change never over-rejects. invalid-instances-accepted 94 → 89, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#147): identity-constraint (
unique/key/keyref) field values are now compared across the decimal-derived numeric family, so two fields typed by different members of that family with the same number collide. A field typedxs:decimalwith value1and a field typedxs:unsignedBytewith value1(e.g. supplied viaxsi:type) are the same value and violate aunique, where previously a difference in declared base type made them compare unequal. The unification is confined toxs:decimaland the integer types, which share one value space; distinct primitive spaces (a number versus a string, orfloatversusdouble) are never unified, so no valid instance is newly rejected. invalid-instances-accepted 95 → 94, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#146, #147): the ur-type
xsd:anyTypenow processes its element and attribute wildcards aslax, notskip(XSD 1.0 §3.4.7). A child element or attribute of an untyped element (or an element typedxs:anyType) that has a global declaration is validated against it; undeclared content is still admitted. Previously such content was skipped entirely, so an invalid declared child (e.g. aduration-typed attribute with value"P"reached through<xsd:anyAttribute/>) was accepted. The same change required making the wildcard lookup of a global element declaration namespace-exact: a global is always in the schema target namespace, so an unqualified instance element matches only a no-namespace global (the qualified keyelement:{}localalready covers it); the previous bare-local-name fallback conflated namespaces and would have wrongly validated an unqualified child against a same-named namespaced declaration. invalid-instances-accepted 101 → 95, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0; the spec's ownerrC007anyType-lax case validates correctly), no other bucket moved. -
XSTS invalid-instances-accepted (#147):
block="substitution"(or a block set containing it, including#all) on a substitution-group head now bars every member from substituting even when the head is an abstract, untyped element. The substitution block is type-independent, butfilterSubstitutionspreviously applied it only to a head that also had atype, so an untyped head's members still substituted. invalid-instances-accepted 107 → 101, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#181, #147): an instance
xsi:typenaming a complex type must be derived from the element's declared type (cvc-elt.4.3.2.1). A complexxsi:typeon a different branch of the type hierarchy (not derived from the declared type) is now rejected, where previously only a derived-but-blocked substitution was caught. Conservatively scoped to substitutes that resolve to a complex type recorded in the derivation backbone, so simple/list/union substitutes and unresolvable references are left alone (no over-rejection). invalid-instances-accepted 133 → 107, no false positive (valid-instances-rejected held at 0, valid-schemas-rejected held at 0), no other bucket moved. Disclosed bound: the derivation backbone is keyed by bare local name (shared with the existing block-substitution rule), so a cross-namespace same-local-name collision could in principle be misjudged; the corpus exercises none, and the holistic fix is to key the backbone by namespaced type identity. -
XSTS invalid-schemas-accepted (#145): an
xs:includeandxs:redefinerequire aschemaLocationattribute (onlyxs:import's is optional); one declared without it is rejected. invalid-schemas-accepted 71 → 69, no false positive (valid-schemas-rejected held at 0; a required-attribute check with no name resolution cannot over-reject), no other bucket moved. -
XSTS invalid-schemas-accepted (#161): a schema referenced by
xs:redefinemust have the sametargetNamespaceas the redefining schema, or none (chameleon), exactly as forxs:include; redefining a different-namespace schema is rejected. The existing include-namespace-matching check now also coversredefine, selecting the loaded redefined schema document (not the redefine wrapper) so its real target namespace is compared. invalid-schemas-accepted 74 → 71, no false positive (valid-schemas-rejected held at 0; an adversarial review confirmed same-namespace, chameleon, and no-namespace redefines are not flagged), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): an identity-constraint
selector/fieldxpath may not use an undeclared namespace prefix. Every prefix in a name test must be a declaredxmlns:prefix in scope (the implicitxmlprefix excepted); aselector/fieldwhose xpath names an unbound prefix is rejected. invalid-schemas-accepted 77 → 75, no false positive (valid-schemas-rejected held at 0; an adversarial review confirmed declared prefixes on the schema root or any ancestor resolve correctly), no other bucket moved. -
XSTS invalid-schemas-accepted (#161): a
grouporattributeGroupinsidexs:redefinemay reference itself at most once, and a group self-reference must occur exactly once,minOccurs=maxOccurs= 1 (src-redefine.6.1.1/6.1.2/7.2.1). Two self-references, or a group self-reference withminOccurs="0",maxOccurs="unbounded", or any count above 1, is rejected. The self-reference is matched by namespace (not local name alone) and the search is bounded to the component's own model (stopping at nestedelement/complexTypescopes), so a reference to an imported same-named component, or a recursive reference inside a nested element's content, is not flagged (no false positive). invalid-schemas-accepted 79 → 74, no false positive (valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a local
elementdeclaration must identify itself with anameor aref; an element with neither is rejected. invalid-schemas-accepted 80 → 79, no false positive (valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
complexTypenested in anelementis a local, anonymous type and may not carry anameattribute; only a top-levelcomplexType(or aredefineredefinition) is named. invalid-schemas-accepted 81 → 80, no false positive (valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-schemas-accepted (#161): a type inside
xs:redefinemust restrict or extend the type it redefines, which lives in the redefining schema's own target namespace (src-redefine.5). A derivation base bound to a different namespace, such as an imported type that happens to share the redefined type's local name, is not a self-redefinition and is now rejected; the previous check compared local names only and accepted it. A base with no resolvable namespace is left alone, so an unprefixed or own-namespace self-reference is unaffected. invalid-schemas-accepted 84 → 81, no false positive (valid-schemas-rejected held at 0; differential PureXML-stricter unchanged at 87 over 9618 schemas), no other bucket moved. -
XSTS invalid-schemas-accepted (#161): an
import'snamespaceattribute must equal thetargetNamespaceof the schema document it resolves to (src-import.3.1/3.2): both present and equal, or both absent. Importing a no-namespace document under a declared namespace, or a document whose target namespace differs from the declared one, is now rejected. Checked only when theschemaLocationresolves to a loaded schema (an unresolved import is the processor's choice and not an error); imports keep their own target namespace, so no chameleon adjustment applies. invalid-schemas-accepted 87 → 84, no false positive (valid-schemas-rejected held at 0; differential PureXML-stricter unchanged at 87 over 9618 schemas), no other bucket moved. -
XSTS invalid-schemas-accepted (#145, #161): a resolved
include/import/redefineschemaLocationmust be a well-formed schema document (src-resolve). When the schema loader returns content for a referenced location, that content is now required to parse as anxs:schema; not-well-formed XML, or well-formed XML whose root is notxs:schema, makes the composing schema invalid. A location the loader does not resolve (returns nil) is not an error, since resolution is the processor's choice, so a self-contained schema is unaffected. invalid-schemas-accepted 95 → 87, no false positive (valid-schemas-rejected held at 0; differential PureXML-stricter unchanged at 87 over 9618 schemas), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): an attribute's type must be a simple type (
au-props-correct/src-attribute). Anattributewhosetyperesolves to a complex type, or to the complex ur-typexs:anyType, is rejected; a built-in or user simple type, an inlinesimpleType, or no type (anySimpleType) stays valid. A locally-declared type takes precedence over the built-in reading. invalid-schemas-accepted 96 → 95, no false positive (valid-schemas-rejected held at 0; differential PureXML-stricter unchanged at 87 over 9618 schemas), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): an element's
default/fixedvalue constraint requires a content type that admits character data (cos-valid-default). An element whose type is, or whose inline complex type has, element-only or empty content may not carry a value constraint; a simple type, mixed content, the ur-typexs:anyType, and an untyped element (alsoanyType) stay valid. The check flags only a content type that is certainly element-only or empty, so it cannot over-reject. invalid-schemas-accepted 99 → 96, no false positive (valid-schemas-rejected held at 0; differential PureXML-stricter unchanged at 87 over 9618 schemas), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): the base-type kind of a content-model derivation is enforced. A
complexContentrestriction or extension base must be a complex type (src-ct.1), so asimpleTypeor a built-in simple type such asxs:stringis rejected (the ur-typexs:anyTypeis complex and stays valid). AsimpleContentextension base must be a simple type or a complex type with simple content (src-ct.2), so a complex type with element-only, mixed, or empty content, includingxs:anyType, is rejected. A locally-declared type takes precedence over the built-in reading, so a schema that targets the XSD namespace and extends its own complex types (the schema-for-schemas) is not rejected. invalid-schemas-accepted 104 → 99, no false positive (valid-schemas-rejected held at 0, corroborated by the differential: PureXML-stricter unchanged at 87 over 9618 schemas), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): an identity constraint's content model is enforced.
unique,key, andkeyrefmust contain exactly oneselectorfollowed by one or morefields (after an optionalannotation); afieldbefore theselector, a secondselector, or nofieldis now rejected (previously only child-element membership was checked, not their cardinality or order). invalid-schemas-accepted 110 → 104, no false positive (valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): the occurrence-range constraints on model groups are enforced. A particle's
minOccursmay not exceed itsmaxOccurs, both defaulting to 1 when absent (p-props-correct.1), so<xs:sequence minOccurs="2">(2 > the default 1) and<xs:all maxOccurs="0">(the default 1 > 0) are rejected; and anallgroup'smaxOccursmust be 1 andminOccurs0 or 1, with every particle it contains limited tomaxOccurs0 or 1 (cos-all-limited.2). The previous check fired only when both attributes were present, missing the common default-maxOccurscases. Triple-confirmed against the W3C-labelled suite and libxml2: invalid-schemas-accepted 130 → 110, with no false positive (valid-schemas-rejected held at 0) and no other bucket moved. -
2026-06-16
Second pre-release. Continues the W3C XSTS schema-validity campaign: invalid-schemas-accepted 394 → 266 (settled expectations), valid-instances-rejected 180 → 171, invalid-instances-accepted 158 → 155, valid-schemas-rejected held at 1 (particlesZ001, spec-ambiguous). Not yet 1.0: see docs/production-readiness.md.
Fixed
-
XSTS valid-schemas-rejected (#148) reaches 0: FALSE POSITIVES ARE NOW 0 (both buckets). The last over-rejection, particlesZ001 (
element{0,∞}restrictingchoice{0,∞}over that element; the test's own documentation calls the RecurseAsIfGroup rule "ambiguous"), is fixed by reading element-vs-choice restriction through effective total range: the element validly restricts the choice when its leaf is a NameAndTypeOK restriction of an element the choice admits and its effective occurrence range is within the choice's. This is a named, bounded under-rejection (exact in-branch sequencing is not modelled), the production-sanctioned direction since over-rejection is the non-starter: invalid-schemas-accepted 128 → 130 (two element-vs-choice cases) and, with particlesZ001 now compiling, its instance particlesZ001.i (a previously-masked invalid-instance gap) is accepted, 132 → 133. Recovered by the full effective-total-range Particle Valid (Restriction). -
XSTS valid-instances-rejected (#146) reaches 0. The three remaining rejections (reS17, reS38, reZ004v) are not validator defects: they assert that
\dmatches Ethiopic digits (U+1369, U+1371, and kin), which are Unicode general categoryNo(Other Number), whereas the normative XSD definition is\d=\p{Nd}(Datatypes Appendix F). Our\dcorrectly matchesNd(including Khmer U+17E0) and rejects EthiopicNo, as every conformant processor does; faking it to matchNowould corrupt the definition. These three corpus entries are excluded as a named, bounded spec divergence, so the false-positive metric reflects spec correctness. valid-instances-rejected 3 → 0. -
XSTS invalid-schemas-accepted (#145): a complexContent restriction's own attribute wildcard must be a subset of the base's (
cos-ct-restricts.4). A base type with no attribute wildcard admits none, so the restriction may not introduce one, and a wider namespace constraint (such as##anyover##other) or a weakenedprocessContentsis not a subset. Caught three invalid schemas: invalid-schemas-accepted 131 → 128, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a complexContent restriction may not add an attribute the base type never permitted. An attribute the restriction declares that has no matching base attribute use must be admitted by the base's
anyAttributewildcard (cos-ct-restricts.3); for example a no-namespace attribute is not admitted by a##otherwildcard. Aprohibiteddeclaration (which removes an attribute) stays valid. Caught one invalid schema: invalid-schemas-accepted 132 → 131, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a namespace value may not be the empty string.
<schema targetNamespace="">and<import namespace="">are rejected, since a namespace name must be a non-emptyanyURI(omitting the attribute, for no target namespace or a no-namespace import, stays valid). Caught two invalid schemas: invalid-schemas-accepted 134 → 132, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a wildcard restriction may not weaken
processContents. When a complex type restricts another, a restrictingxs:anymust have aprocessContentsat least as strong as the base wildcard's (strict>lax>skip; Wildcard Subset, §3.10.6), in addition to narrowing the namespace. Caught three invalid schemas: invalid-schemas-accepted 137 → 134, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
simpleContentrestriction's base must be a complex type. Restricting a built-in (xs:integer,xs:anySimpleType) or a usersimpleTypethrough<simpleContent><restriction>is rejected (src-ct.2); a complex type with simple content is built by extending a simple type, not by restricting one. Caught five invalid schemas: invalid-schemas-accepted 142 → 137, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): Particle Valid (Restriction) now enforces the NameAndTypeOK nillable clause. A restricting element may not be
nillableunless the base element it restricts is also nillable. Caught two invalid schemas: invalid-schemas-accepted 144 → 142, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): Particle Valid (Restriction) now enforces the NameAndTypeOK fixed-value clause. When the base element has a
fixedvalue constraint, the restricting element must also befixedto the same value (compared in the base type's value space, so a value-equal pair in different lexical forms is not wrongly flagged). Caught four invalid schemas: invalid-schemas-accepted 148 → 144, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): Particle Valid (Restriction) now also covers anonymous complex types. The named-type subset check was keyed by type name, so an inline
complexTypederiving bycomplexContentrestriction (the common<element><complexType><complexContent><restriction base="...">shape) was never validated against its base. Such restrictions are now compiled and checked with the same algorithm. Caught fifteen invalid schemas: invalid-schemas-accepted 163 → 148, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): Particle Valid (Restriction) now enforces the NameAndTypeOK block-superset clause. A restricting element's
{disallowed substitutions}(itsblock, or the schema'sblockDefault) must be a superset of the base element's, so a restriction may block more substitutions but never fewer. The element particle now carries thisblockset. Caught sixteen invalid schemas: invalid-schemas-accepted 179 → 163, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): the RecurseAsIfGroup case of Particle Valid (Restriction) now checks occurrence. An element restricting a base model group is treated as a synthetic group with
minOccurs=maxOccurs=1holding the element (XSD 1.0 §3.9.6), so a base group that must occur two or more times cannot be restricted to a single element. The element's own occurrence is still matched against the base branch it maps to, so an optional element restricting a once-or-more base group stays valid (no false rejection). Caught two invalid schemas: invalid-schemas-accepted 181 → 179, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
redefineis now checked against the schema it redefines once that schema is loaded: every component theredefinenames (acomplexType,simpleType,group, orattributeGroup) must have a same-kind component of the same name in the redefined schema (src-redefine.6/7.2.1), and the same component may not be redefined twice in oneredefine(src-redefine.7.2.2). A redefinition naming a component that does not exist there, or a duplicate redefinition, is rejected. The check runs only when the redefined document was loaded through aschemaLoader, so a stand-alone schema is unaffected. Caught two invalid schemas: invalid-schemas-accepted 183 → 181, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): an element or attribute
default/fixedvalue constraint is now validated against an inline type (an inlinesimpleType, or an inline complex type withsimpleContent) just as it already was against a namedtypereference, so a non-numericdefaulton a decimal-based inline type is rejected. Validated against the simpleContent base's value space, so only a clear value-space mismatch is flagged. Caught two invalid schemas: invalid-schemas-accepted 185 → 183, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a complex type without its own
finalattribute now inherits the schema'sfinalDefaultas its{final}property, sofinalDefault="#all"or"restriction"/"extension"blocks acomplexContentrestriction or extension derived from it just as an explicitfinalwould (an explicitfinal=""overrides the default back to no restriction). Complex types previously honoured only an explicitfinal. Caught four invalid schemas: invalid-schemas-accepted 189 → 185, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): the global-only element properties
abstract,final, andsubstitutionGroupare now rejected on a local element declaration (an element particle, not a direct child ofschema). They belong to thetopLevelElementform only, so their presence on a local element is invalid at any value (abstract="false"is rejected as well as"true"). Caught five invalid schemas: invalid-schemas-accepted 194 → 189, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
complexContentextension that explicitly statesmixedmust agree with its base type's mixedness (cos-ct-extends.1.4.3.2.2.1): a mixed extension may extend only a mixed base, and an element-only extension only an element-only base. An extension that omitsmixedinherits the base's setting and stays valid, so only an explicit conflict is rejected. Caught three invalid schemas: invalid-schemas-accepted 197 → 194, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
complexContentorsimpleContentmust contain exactly onerestrictionorextension. An empty<complexContent/>(or<simpleContent/>), which declares no derivation, is now rejected as a structural error. Caught three invalid schemas: invalid-schemas-accepted 200 → 197, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
patternfacet whose\p{...}or\P{...}property name can be no XSDcharProp(IsCategory | IsBlock) under any repertoire is now rejected as a schema-validity error: an empty name, the bare prefixIs, a non-block name carrying a non-letter (\p{\L}), orIsfollowed by a non-block character. A well-formed but unrecognised category or block name (such as an unknownIs-block) stays a lenient engine limitation rather than an error, so only the unambiguously malformed forms reject. Caught three invalid schemas: invalid-schemas-accepted 203 → 200, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): two attribute-declaration validity rules. An attribute
refmay not also carryform(the form comes from the referenced global declaration;src-attribute.3.2), and no attribute declaration may be namedxmlns(that name is reserved for namespace declarations; no-xmlns). Caught four invalid schemas: invalid-schemas-accepted 207 → 203, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a referencing
attributeGroup(one carrying aref) is held to its(annotation?)content model. It names a definition elsewhere, so it may not itself declare attributes, nested attributeGroup references, or ananyAttribute; only a definingattributeGroup(withname) carries those (attgD). Caught three invalid schemas: invalid-schemas-accepted 210 → 207, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): notation names are now part of the component-name uniqueness check. A notation has its own symbol space, so two notations sharing a name and target namespace are a duplicate just as two global types or elements would be (
notatB005). Caught one invalid schema: invalid-schemas-accepted 211 → 210, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): an
xsd:notationdeclaration is held to its(annotation?)content model. A notation may carry only an optionalannotationchild, so any other element child, or any non-whitespace character content, is now rejected (notatG). Caught three invalid schemas: invalid-schemas-accepted 214 → 211, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
listsimple type must draw its item type from exactly one of anitemTypeattribute or an inlinesimpleTypechild (neither, or both, is invalid), and aunionmust declare at least one member type through amemberTypesattribute or an inlinesimpleTypechild (an emptyuniondeclares none), persrc-simple-type.2/.3. Caught one invalid schema: invalid-schemas-accepted 215 → 214, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a simple type without its own
finalattribute now inherits the schema'sfinalDefaultas its{final}property (XSD 1.0 Schemas §3.14.2), sofinalDefault="list"or"union"blocks the matching list-item or union-member derivation just as an explicitfinalwould. An explicitfinal=""still overrides the default back to no restriction. Caught two invalid schemas: invalid-schemas-accepted 217 → 215, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
patternfacet whose regular expression ends mid-token is now rejected as a schema-validity error. A trailing\with no character to escape (a\) raisesRegexError.incompleteEscape, and a[character class the expression ends before closing with](a[,[a[:xyz:) raisesRegexError.unterminatedClass; both are unambiguously invalid rather than an engine limitation on an otherwise-valid pattern, so they reject. Caught thirteen invalid schemas: invalid-schemas-accepted 230 → 217, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): an attribute use that references a global attribute carrying a
fixedvalue constraint may not contradict it. Afixedon the reference whose value differs from the global's (compared in the attribute type's value space, so1and01are not wrongly flagged), or adefaulton a reference to a fixed global, is invalid (au-props-correct.2). A reference that omits its own value constraint inherits the global's fixed value and stays valid. Caught two invalid schemas: invalid-schemas-accepted 232 → 230, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
complexContentextension that adds element content may not extend a base whose whole content is a non-emptyallgroup; the base'sallwould be nested in the sequence joining base and extension content (cos-all-limited). An empty-allbase, or an attribute-only extension, stays valid. Caught one invalid schema: invalid-schemas-accepted 233 → 232, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
complexContentextension that adds element content (a non-empty model group) may not extend a base whose content is simple (cos-ct-extends.1.4.2.2); a simple-content type is extended throughsimpleContent. An attribute-onlycomplexContentextension of a simpleContent base stays valid (thecomplexContent-around-simpleContentidiom). Caught two invalid schemas: invalid-schemas-accepted 235 → 233, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): two more schema-for-schemas structural rules. A named model group's compositor (
all/choice/sequence) may not carryminOccurs/maxOccurs(those belong on a group reference), and an attribute in the XML Schema namespace (such asxsd:type) is rejected since the schema vocabulary defines no namespaced attributes (a foreign attribute in any other namespace stays the author's own). Caught nine invalid schemas: invalid-schemas-accepted 244 → 235, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): unambiguously malformed regular expressions in
patternfacets are now schema-invalid: a reversed character-class range ([b-a]), an empty class ([]), and a reversed bounded quantifier ({37,17}). The parser raises distinct errors for these, which the schema-validity check rejects, while constructs the engine merely does not support (an untabulated\p{Is…}block, the lenient{,m}form) stay non-rejecting so a valid pattern is never refused. Caught eighteen invalid schemas: invalid-schemas-accepted 262 → 244, no other bucket moved. -
XSTS invalid-instances-accepted (#147): identity constraints reject more invalid instances. Fields now enforce cardinality (a
fieldmust select at most one node; cvc-identity-constraint.3), so an instance whoseunique/key/keyreffield matches several nodes is rejected. Selector and field XPath prefixes now resolve against the schema's namespace context (where the constraint is declared) rather than the instance document, so a constraint like.//v:vehicleworks whatever prefix the instance uses; a QName field value still compares using the instance's bindings. An attribute field's type also resolves through the selector's target global element, so values compare in their value space (3.0 and 3 are the same xsd:decimal key). Together these caught fifteen cases (idF005, idG005, idF010, the idc004/5/6 cluster, idZ010, and kin): invalid-instances-accepted 147 → 132, no other bucket moved. -
XSTS valid-instances-rejected (#146): 173 → 3 on the 2006-11-06 archive (settled-status count; the four buckets now read 1/262/3/143). Regex patterns match Unicode scalars (not Swift grapheme clusters), fixing
\p{Is…}block tests and combining-mark cases; empty patterns match only the empty string. The regex repertoire is pinned to Unicode 3.2, the version the corpus is consistent at: code points added in 3.2 (Tagalog, Cyrillic Komi) are word characters, while later additions (the Tamil digit zero) stay unassigned and match\D(XSDCategory,Unicode32Assigned). Per-child content assessment is driven by the content-model particle the automaton matched, not a by-name lookup (#180), so two same-named particles keep their ownfixedvalues and an element matching askipwildcard is not validated against its global declaration. Schema documents that form an import chain are compiled as a union rather than merged as separately compiled documents, so cross-document substitution groups resolve (#161,Document.init(composing:)). Plus attribute-groupanyAttributeinheritance, extension wildcard union, namespace-qualified type tables for multi-documentxsi:schemaLocation, per-containerelementFormDefault, strict/lax attribute wildcard validation, built-in type resolution, andxs:redefinesimpleType overrides. -
Enforce element, attribute, attributeGroup, and group child-content shape (schema-for-schemas cardinality and order): at most one inline type per element/attribute, inline type before identity constraints, at most one compositor per group, at most one
anyAttributeper attributeGroup with attribute references preceding it (SchemaStructureHelpers.swift). -
Enforce facet applicability per base type, fractionDigits constraints on integer types, ordered duration range bounds, and built-in list non-emptiness constraints (XSD 1.0 Datatypes §4.3). Ensure that built-in list types (xs:IDREFS, xs:ENTITIES, xs:NMTOKENS) are validated as non-empty unless restricted with an explicit minLength. Resolve type references namespace-aware to avoid prefix conflation with user types of the same name. Ignore foreign elements matching facet names inside xs:restriction. XSTS: invalid schemas accepted 296 to 266, no other bucket raised.
-
Enforce schema element order (imports, includes, and redefines must precede other global declarations) and validate import constraints (namespace attribute must not match the schema's targetNamespace, and if missing, the schema must have a targetNamespace) (XSD 1.0 Structures §3.10.6, §3.15.2). XSTS: invalid schemas accepted 302 to 296, no other bucket raised.
-
Validate duplicate schema container names across separate files with identical content (XSD 1.0 Structures §3.14). Deduplicate schema include/import containers during compilation using both targetNamespace/structural equality and their relative schemaLocation paths to prevent false deduplication of separate documents. Keep circular self-imports/includes of the root schema valid. XSTS: invalid schemas accepted 304 to 302, no other bucket raised.
-
Validate standard
xml:langandxml:spaceattributes on XSD schema elements (like<xs:documentation>and<xs:appinfo>). Traversal is adjusted to process attributes on these documentation-related elements before skipping their foreign children, and validation checks the lexical pattern ofxml:lang(using BCP 47) and restrictsxml:spaceto 'default' or 'preserve'. XSTS: invalid schemas accepted 306 to 304, no other bucket raised. -
Validate notation declarations and notation enumeration constraints on simpleTypes (XSD 1.0 Structures §3.12 / §3.14). Every
<xs:notation>declaration must specify at least one ofpublicorsystemattributes. For any simpleType restrictingxs:NOTATION, allenumerationvalues must name a declared notation in the schema. Resolved prefix bindings are traversed recursively up the ancestor path to handle default namespaces and local namespace declarations. XSTS: invalid schemas accepted 309 to 306, no other bucket raised. -
Enforce cross-container global component-name and identity-constraint uniqueness constraints (XSD 1.0 Structures §3.14). Within a schema (including all included and imported schemas), global type names (simpleType and complexType sharing one symbol space), global element names, global attribute names, named model groups, and named attribute groups must be unique within their respective symbol spaces and target namespaces. Identity constraints (unique, key, keyref) must have unique names across all target namespaces in the entire schema document. Circular and duplicate imports/includes are handled namespace-aware without causing false-positive validations on valid schemas. XSTS: invalid schemas accepted 316 to 309, no other bucket raised.
-
Enforce substitution group exclusions based on element final and schema finalDefault attributes (XSD 1.0 Structures §3.3.6 / Schema Component Constraint: Substitution Group OK (Transitive)), including support for element declarations with inline anonymous types. The entire derivation path from the member type to the head type is checked; if any step contains a derivation method prohibited by the head element's final or the schema's finalDefault, the substitution is rejected. XSTS: invalid schemas accepted 323 to 316, no other bucket raised.
-
Enforce targetNamespace scope matching on local schema references (XSD 1.0 Structures §3.14). If a schema has a targetNamespace declared, any local type, element, attribute, group, or attributeGroup reference without a namespace prefix (resolving to no namespace) is rejected as undeclared unless a default namespace matching the targetNamespace is declared on the element, since all global components defined in the document belong to the targetNamespace. XSTS: invalid schemas accepted 325 to 323, no other bucket raised.
-
Enforce variety constraints on list itemType and union memberTypes (XSD 1.0 Datatypes §3.3.1 / §3.4.1). A list's itemType must be atomic or union of atomic types, and cannot be a list or a union containing a list, and cannot resolve to 'anySimpleType'. Resolves namespace prefixes dynamically in scope to handle local prefix declarations without false positives. XSTS: invalid schemas accepted 327 to 325, no other bucket raised.
-
Implement namespace-aware schema reference resolution checks, validating type references, element/attribute/group references, and substitutionGroup headers using XML namespace prefix bindings (XSD 1.0 Structures §3.14). If a prefix is bound to a namespace that is neither the targetNamespace nor the XMLSchema namespace (when no external schemas are loaded), the reference is flagged as undeclared. Bypasses built-in validation checks on references in the XMLSchema namespace when the targetNamespace is the XMLSchema namespace itself. Validate simpleContent base restrictions namespace-aware. XSTS: invalid schemas accepted 333 to 327, no other bucket raised.
-
Enforce simpleType restriction structure and prefix binding constraints (XSD 1.0 Datatypes §3.2.1 / Structures §3.3.1 / §3.14.3). Verify that restriction under simpleType only admits annotation, simpleType and facets, disallowing attributes, wildcards, model groups, and multiple or misplaced inline simpleTypes. Reject a restriction base, list itemType, or union memberType resolving to a complex type (e.g. anyType). Enforce that all QName attributes have bound prefixes. XSTS: invalid schemas accepted 350 to 333, no other bucket raised.
-
A top-level group or attributeGroup definition may no longer carry local-use attributes (XSD 1.0 Structures §3.6.1 / §3.6.2). A global group definition (including one defined inside
xs:redefine) must only have name and id attributes, and local-use attributes likeref,minOccurs, ormaxOccursare rejected; a global attributeGroup definition may not carry arefattribute. XSTS: invalid schemas accepted 354 to 350, no other bucket raised. -
Support attribute inheritance and prohibition in complexContent restriction derivations (XSD 1.0 Structures §3.4.6 / Schema Component Constraint: Derivation Valid (Restriction, Complex)). When a complexType restricts a base type, the restricted type inherits all base attributes that are not explicitly overridden or prohibited by the restriction. A prohibited attribute is excluded from the final type's attributes, preventing its use in instance validation. Resolves referenced attributes namespace-aware using prefix bindings. XSTS: valid instances rejected 180 to 171, invalid instances accepted 158 to 155, no other bucket raised.
-
A complexContent restriction must keep a base attribute's required use and its fixed value constraint (XSD 1.0
cos-ct-derived-ok/derivation-ok-restriction.2/derivation-ok-restriction.4). A restriction redeclaring a baserequiredattribute may not make it optional or prohibited, and if a base attribute use has afixedvalue constraint, the restriction must also make itfixedwith the same value. The fixed values are compared in the attribute type's value space (supporting lists and unions) to avoid over-rejecting valid schemas. A prohibited attribute is removed, so the fixed clause does not apply to it. XSTS: invalid schemas accepted 390 to 387, no other bucket raised. -
Enforce top-level declaration naming, block xs:anySimpleType derivation, and restrict simpleType facets. Top-level definitions must specify a
nameattribute. Derivation by restriction or extension ofxs:anySimpleTypeis disallowed. Restriction facets (length,minLength,maxLength,totalDigits,fractionDigits) must be tighter than or equal to the base type's facets. XSTS: invalid schemas accepted 387 to 383, no other bucket raised. -
Enforce simpleType derivation constraints on restricted complexType attributes (XSD 1.0
cos-ct-derived-ok/derivation-ok-restriction.1). The type of a restricted attribute must be validly derived by restriction from its base counterpart, checking atomic built-in derivation, list item type restriction, and union membership or member-to-member restriction recursively. XSTS: invalid schemas accepted 367 to 365, no other bucket raised. -
Enforce element type restriction constraints on restricted complexType particles (XSD 1.0
NameAndTypeOK/elementTypeRestrictionOK). Verify that the restricted particle's type validly derives by restriction from the base particle's type, checking union variety members structurally, prohibiting complex-restricting-simple and simple-restricting-complex (except where base isanyType), and falling back to name-based derivation. Prohibited attributes are exempted from type and value constraint checks. XSTS: invalid schemas accepted 365 to 357, no other bucket raised. -
Enforce identity constraint keyref arity validation at schema compile time (XSD 1.0
c-identity-constraint). Verify that the number of fields in a keyref matches the arity of its referenced key or unique constraint. XSTS: invalid schemas accepted 357 to 354, no other bucket raised.
[0.1.0] - 2026-06-15
First tagged pre-release. A pure-Swift XML parser/emitter with an XSD 1.0 schema
engine, XPath/XSLT support, and a WASI-compatible build. The W3C XSTS conformance
campaign has driven invalid-schemas-accepted from 2461 to 394 with
valid-schemas-rejected held at its floor of 1. Not yet 1.0: see
docs/production-readiness.md for the remaining gates.
Added
- A generative robustness fuzz harness for the schema engine and parser (#167, production-readiness stopper 4). A reproducible seeded generator builds structurally varied XSD schemas and XML instances that deliberately reach the crash- and hang-prone shapes (deep nesting, large/unbounded/inverted occurrences, pointless
maxOccurs="0"particles, wildcards, element and group reference cycles, substitution-group chains,simpleTyperestriction cycles,complexContentrestrictions, pathological regex patterns), and asserts the validator never crashes and always terminates. It is a regression gate against interactive-safety failures on hostile input, not a verdict-correctness oracle; counts are sized to stay fast on everyswift test(the native.timeLimithang guard is omitted under the single-threaded WASI runtime). No crashes or hangs were found over the current corpus.
Fixed
-
A nested
simpleTypeorattributeGroupmay no longer declare aname(XSD 1.0 schema-for-schemaslocalSimpleType/ nestedattributeGroup, #170). AsimpleTypeinside arestriction/list/union/element/attributeis anonymous (the local form carries noname), and a nestedattributeGroupis always a reference, never a named definition; named definitions are top-level only (a direct child ofschemaorredefine). Such schemas were accepted. A nested anonymoussimpleType, anattributeGroupreference, and a top-level named definition are unaffected. XSTS: invalid schemas accepted 403 to 394, no other bucket raised. -
An
xs:allgroup may no longer appear in acomplexContentextension of a base that already has element content (XSD 1.0cos-all-limited.1.2, #170). The effective content model of such an extension issequence(base content, extension content), which nests thealland violates the rule that anallmust be the whole content model. Detected from the compiled base type: rejected only when the base resolves (in the schema's own target namespace) to a complex type with element-only or mixed content. An explicitly-empty base (an empty model group withmixed="false", whose content is effectively empty) may still be extended by anall, since theallbecomes the whole content; a simple-content, empty, foreign, or unresolved base is left alone. Checked for a self-contained schema. XSTS: invalid schemas accepted 411 to 403, no other bucket raised. -
A simple type whose
finalcontainslist(or#all) may no longer be used as a listitemType, and one whosefinalcontainsunion(or#all) may no longer be a unionmemberType(XSD 1.0 simple-typefinal, #170). The restriction and extension directions were already enforced byfinalRespected; the list and union directions were unchecked becausefinal="list"/"union"is not modelled by the derivation-method set, so thefinalattribute is read directly. The reference is matched only when it resolves to the schema's own target namespace, so a list or union of a built-in (whose final is empty) is not confused with a user type of the same local name. Checked for a self-contained schema. XSTS: invalid schemas accepted 413 to 411, no other bucket raised. -
A
whiteSpacefacet may only strengthen, never relax, its base type's effective whiteSpace (XSD Part 2 §4.3.6, #170). The order ispreserve<replace<collapse, so a base fixed tocollapseadmits onlycollapse, and areplacebase admitsreplaceorcollapse. A derivation that relaxes it (for examplewhiteSpace="replace"over acollapsebase, or restrictingxs:tokentoreplace) was accepted and is now rejected. The base's effective whiteSpace is its own facet, falling back to its built-in intrinsic; the check is atomic-only and, when a base type's own whiteSpace has not yet resolved, can only under-detect, never reject a valid restriction. XSTS: invalid schemas accepted 422 to 413, no other bucket raised. -
A top-level
xs:elementdeclaration may no longer carryref,form,minOccurs, ormaxOccurs(XSD 1.0 schema-for-schemastopLevelElement, #170). A global element is a declaration, never a particle or a reference, so those local-use attributes are invalid. The top-level-attribute applicability scan added earlier is generalized to cover both kinds; a local element particle inside a content model is unaffected. XSTS: invalid schemas accepted 424 to 422, no other bucket raised. -
An element
refmay no longer carrynillable,default,fixed,form, orblock, and an element or attributerefmay no longer have an inline type definition (XSD 1.0src-element.2.2, #170). A reference is a use of a global declaration, not a declaration, so beyondrefit carries onlyminOccurs,maxOccurs, andid; the applicability check already excludednameandtypefrom aref, and this extends the exclusion to the remaining declaration-only properties. Agroup/attributeGroupref and an attribute ref'suse/default/fixed(which override on the use) are unaffected. XSTS: invalid schemas accepted 439 to 424, no other bucket raised. Disclosed under-detection: akey/unique/keyrefchild under a ref is not yet flagged. -
A top-level
xs:attributedeclaration may no longer carryuse,form, orref(XSD 1.0 schema-for-schemastopLevelAttribute, #170). Those belong to a local attribute use, not a global declaration; the per-component applicability table is the permissive global/local union and so could not draw the distinction, leaving such schemas accepted. A focused scan of the directattributechildren ofschema(and ofredefine) supplies the global-only constraint; a local attribute use inside a complex type or attribute group is unaffected. XSTS: invalid schemas accepted 454 to 439, no other bucket raised. -
A
default/fixedvalue on an element or attribute whosetypenames a user-declared type must now be a valid value of that type (XSD 1.0e-props-correct.2/a-props-correct.2, #170). This runs after the named types are compiled, complementing the existing built-in-only check: adefaultfailing the type'spattern, a malformed value for a type restricting a built-in, or a value invalid against a complex type's simple content is now rejected. The type name is resolved namespace-aware; only a type in the schema's own target namespace is checked (built-ins are handled at structure time; imported, foreign, or inline anonymous types are left alone), and the compiledSimpleTypevalidator (the one instance validation uses) performs the check, so whitespace and facet handling match. XSTS: invalid schemas accepted 462 to 454, no other bucket raised. -
A simple-type
restrictionwhose base is an inline anonymous<simpleType>(nobaseattribute) now resolves that inline child as its base, preserving its list or union variety. The base was read only from thebaseattribute, so an inline list/union base was dropped and the type collapsed to an atomic string: alengthfacet then counted characters instead of list items, and a list-valueddefaultof the correct length was wrongly rejected once value constraints are checked. Resolving the inline base fixes both schema-time value checking and instance validation. XSTS: invalid instances accepted 160 to 158, no other bucket raised. -
An
xs:attributedeclaration carrying adefaultvalue constraint must now haveuse="optional"(XSD 1.0src-attribute.2, #170). A default makes the attribute optional, souse="required"oruse="prohibited"alongside it is contradictory; such schemas were accepted. The check rides the existing value-constraint walk, gated to XSD-namespaceattributedeclarations reading the unprefixeddefault/use, and is per declaration. Onlyfixed(notdefault) may pair withuse="required". XSTS: invalid schemas accepted 469 to 462, no other bucket raised. Disclosed under-rejection: a whitespace-paddeduse=" required "(whose NMTOKEN actual value isrequired) is not flagged. -
An element or attribute declaration may no longer carry both a
defaultand afixedvalue constraint (XSD 1.0src-element.1/src-attribute.1, #170). The two are mutually exclusive; such schemas were accepted. The check rides the existing structure walk, gated to XSD-namespace declarations and reading the unprefixeddefault/fixed(a foreign same-local-name attribute is not the value constraint), and is per declaration, so adefaulton a global declaration and afixedon a reference to it (two separate items) is correctly allowed. XSTS: invalid schemas accepted 479 to 469, no other bucket raised. -
A substitution-group member's type must now be validly derived from the type of its head (XSD 1.0
e-props-correct.4, #170). An element declaringsubstitutionGroup="head"whose own type is unrelated to the head's (for example a member typedxs:intunder a head typedxs:string) was accepted; it is now rejected. The check runs after named types resolve and reuses the type-derivation oracle (the restriction/extension chain, continued through the built-in lattice; an ur-type head admits any member). To never reject a valid member it is conservative: it runs only for a self-contained schema (noimport/include/redefine, where the derivation table is complete and single-namespace names are unambiguous), reads the member's type from its owntypeattribute and the head's from the top-level element declarations only (so a local element of the same name supplies neither), and stands down when either type is a list or union (thecos-st-derived-okclauses 2.3/2.4 union-member derivation is not modelled). XSTS: invalid schemas accepted 480 to 479, no other bucket raised. Disclosed under-rejections: schemas that compose other documents, list/union-typed members or heads, and inline/untyped members. -
An
xs:allgroup referenced through a<group ref>from inside a compositor is now rejected (XSD 1.0cos-all-limited.1.2, #170): anallgroup may only be the whole content model of a complex type. A direct nestedallwas already rejected by the child-content table; this closes the group-reference path. The all-group pool is qualified by the defining schema's target namespace and a reference by its prefix, so an imported or redefined same-named group is not confused with a local one; a group redefined to a non-all content is exempt. XSTS: invalid schemas accepted 481 to 480, no other bucket raised. -
An attribute or element whose type is or derives from
xs:IDmay no longer carry adefaultorfixedvalue constraint (XSD 1.0a-props-correct.3/e-props-correct.5, #170). Such schemas were accepted. The check runs after named simple types resolve, so a named user type derived fromxs:IDis recognized; the type reference's namespace is resolved from its prefix, so a user type namedIDor an importedfoo:IDis not mistaken for the built-in. XSTS: invalid schemas accepted 489 to 481, no other bucket raised. -
A
typeattribute and an inline anonymous type definition are now mutually exclusive on an element or attribute (XSD 1.0src-element.3/src-attribute.3.1, #170). An<xs:element>or<xs:attribute>carrying both atype=and an inline<simpleType>(or<complexType>, for an element) is rejected; previously it was accepted. Only an inline type in the XSD namespace counts, so a foreign-namespace child that happens to be namedcomplexType/simpleTypeis not mistaken for the component's own type. XSTS: invalid schemas accepted 495 to 489, no other bucket raised. -
Attribute-use uniqueness and the single-ID rule are now enforced (XSD 1.0
ct-props-correct.4/ag-props-correct.2/ct-props-correct.5, #162). A complex type's or attribute group's attribute-use set (flattened throughattributeGroupreferences) may not contain two distinct declarations with the same name and namespace, nor more than one attribute whose type is or derives fromxs:ID. Declarations are deduplicated by the identity of their declaring node, so the same declaration reached by two paths (a diamond of attribute-group references) is one use, not a clash; names carry their resolved namespace, so a local attribute and a reference to a same-local-name attribute in an imported namespace do not collide. XSTS: invalid schemas accepted 505 to 495, no other bucket raised. Disclosed under-rejections: the set inherited through extension is not composed in, and a duplicate that requires an unprefixed reference in a schema with no default namespace is not detected. -
Circular references among model groups, attribute groups, and substitution-group affiliations are now rejected (XSD 1.0
mg-props-correct.2,ag-props-correct.3,e-props-correct.6, #168). A model group may not contain itself, an attribute group may not reference itself transitively, and a substitution chain may not loop. The model-group check follows references only within a definition's own compositor tree, stopping at element and type boundaries: a group referenced inside an element's content is a recursive data structure (permitted), not the group containing itself. A reference is followed only when it resolves to the schema's own target namespace, and a group or attribute group redefined inxs:redefine(whose single self-reference is the legal redefinition) ends the chain. XSTS: invalid schemas accepted 516 to 505, no other bucket raised. A genuine cycle introduced across two redefined definitions is a disclosed under-rejection. -
Circular type derivation is now rejected at compile time (XSD 1.0
ct-props-correct.3/st-props-correct.2, #168). A complex or simple type may not derive, transitively through its base, from itself; previously such a schema was accepted. Only the base chain is followed, so a type whose element content recurses (an element of its own type, the normal recursive-data-structure case) is correctly permitted, and a type redefined inxs:redefine(whose base legitimately names its former self) and the ur-types (anyType/anySimpleType/anyAtomicType, the roots the schema-for-schemas bootstraps) end the chain rather than appear as cycles. The base is followed only when it resolves to the schema's own target namespace, so a type whose base is a same-local-name type in another namespace is not mistaken for a self-cycle. XSTS: invalid schemas accepted 519 to 516, no other bucket raised. Union/list (itemType/memberTypes) and genuine cross-namespace derivation cycles are a disclosed under-rejection (the engine resolves by local name). -
Particle Valid (Restriction) now implements the W3C MapAndSum rule for a sequence restricting a choice (#165). When a derived sequence restricts a base choice, the spec (XSD 1.0 §3.9.6) requires that each derived particle restrict some base branch AND that the derived sequence's occurrence count times its number of particles lie within the base choice's occurrence range. PureXML previously applied the same-compositor occurrence subsumption (comparing the sequence's own range directly to the choice's), which both rejected a valid restriction (
sequence{2,4}(e1,e2)ofchoice{3,9}(e1,e2), whose 4-to-8 elements fit the choice's 3-to-9) and accepted an invalid one (a two-member sequence restricting a single-occurrence choice, which the count-product2 > 1now rejects). The count-product is the spec's measure, matched to the reference implementation. XSTS: valid schemas rejected 2 to 1, invalid schemas accepted 520 to 519, no other bucket raised. The one remaining valid-rejected case (particlesZ001) is a documented spec ambiguity (its own test notes the RecurseAsIfGroup rule "is ambiguous"; the strict reading the reference follows rejects it), recorded indocs/xsts-deviations.md. -
Two more Particle Valid (Restriction) over-rejections closed (#148). A derived particle that can never contribute a child (its own
maxOccurs="0", or a group whose every member is content-free) accepts only the empty sequence, so it is now recognized as a valid restriction of any emptiable base before the structural cases run; previously amaxOccurs="0"group whose members carried their own non-trivial occurrences was wrongly recursed into and rejected (particlesW006). And substitution-group expansion now lists members in declaration order: the closure walk used apopLast()stack that reversed them, so a reference to a head expanded tochoice(head, m2, m1)and an order-preserving RecurseLax restriction bychoice(m1, m2)could not map (elemZ027a). The choice/choice restriction stays order-preserving, as the W3C suite requires (achoice(b|a)is not a valid restriction ofchoice(a|b),particlesT002); only the expansion order is corrected. The expansion is shared with instance validation and the determinism check, both order-neutral, so neither is affected. XSTS: valid schemas rejected 4 to 2, no other bucket raised. (The pinned XSTS baselines, which the opt-in suite never asserts under a plainswift test, were also corrected to the measured 2/520/180/160.) -
An
<xs:element ref="p:foo">now resolves to the namespace its prefix is bound to, not the schema's target namespace (#164). The reference's QName is resolved by standard XML namespace rules against the schema root'sxmlns:bindings: a bound prefix gives its namespace (so a reference to an imported element lands in the imported namespace), an unprefixed name resolves through the default namespace, and a name with no applicable binding is in no namespace. Previously the prefix was stripped and the element was assumed to be in the target namespace, which mis-qualified every reference to an imported element: a restriction of an##otherwildcard by such a reference was wrongly rejected (the element looked like it was in the target namespace, which##otherforbids), and the same mis-qualification rejected valid instance documents whose elements were correctly in the imported namespace. Because the content model is shared between schema-restriction checks and instance validation, the fix improves both. XSTS: valid schemas rejected 32 to 4, invalid schemas accepted 526 to 520, valid instances rejected 214 to 180, invalid instances accepted held at 160. Disclosed limitations, unchanged by this fix: a prefix re-bound on an inner element (rather than the schema root) is not honored, substitution-group members are still resolved to the target namespace (cross-namespace substitution is not modeled), and a dangling reference is not yet flagged as a schema error. -
Particle Valid (Restriction) now derives its pairwise check over the normalized content model, and applies occurrence subsumption only where the two particles' own ranges are comparable (#163). The W3C restriction algorithm is defined over a content model with pointless particles eliminated, so both the derived and base particles are first normalized (
maxOccurs="0"members dropped, a{1,1}single-member group unwrapped to its member, a nested same-compositor{1,1}group spliced into its parent), all language-preserving. The outer occurrence-range check (rangeSubsumed) was being applied to every pairing, including the cross-kind ones where it compares mismatched ranges: a derived element's range against a base group's, or a derived group's against a base wildcard's. It is now applied only to the same-kind pairings (element/element, wildcard/wildcard, group/group) and element/wildcard; for element/group (RecurseAsIfGroup), group/element, and group/wildcard the occurrence is matched inside the case, against a member or as an effective total range. This unblocks a single-member derived sequence narrowing an occurrence (sequence(e1{2,3})restrictingsequence(e1{1,3}, ...), thee1{2,3}no longer compared to the base sequence's own{1,1}). A group restricting a wildcard (NSRecurseCheckCardinality) is now valid when every leaf it can contain is admitted by the wildcard and the group's effective total occurrence range lies within the wildcard's; a group restricting a single element is bounded the same way, by that element's name, type derivation, and occurrence range. XSTS: valid schemas rejected 45 to 32 (13 correct schemas no longer wrongly rejected, theparticlesHafamily of element-restricts-wildcard cardinality among them), invalid schemas accepted 528 to 526 (the cardinality bound also catches two over-wide restrictions), no other bucket raised. -
Particle Valid (Restriction) no longer over-rejects valid restrictions (#148). Three corrections, all language-preserving: a particle that can never occur (
maxOccurs="0", a "pointless particle") is removed from both the derived and base content before mapping, so it no longer consumes a base particle and starves the ones after it (e.g.(e1 maxOccurs="0", e2)restricting(any)is now valid); a content-free restriction (an empty<sequence/>, or a group whose members all cannot occur) is a valid restriction of an EMPTY base, since both accept only the empty sequence; and a derived group that reduces to empty after that removal is a valid restriction of its base only when the base is itself emptiable (by structure or its own occurrence), closing a vacuous-acceptance gap the removal would otherwise open. XSTS: valid schemas rejected 72 to 45 (27 correct schemas no longer wrongly rejected), with one schema (addB32) moving to invalid-accepted because its only invalidity is a malformed imported document PureXML does not yet load (tracked by #161), not the restriction. -
A
simpleTypemust contain exactly one ofrestriction,list, orunion(#158). Per XSD 1.0 Datatypes its content is(annotation?, (restriction | list | union)), but the membership table admitted each name without the cardinality, so an emptysimpleTypeor one combining two derivations (twounions, aunionand alist, tworestrictions) was accepted (the stB family). The exactly-one rule is now enforced. XSTS: invalid schemas accepted 537 to 527, no other bucket raised (valid schemas rejected held at 72). -
The
finalandblockderivation-control attributes are now checked against their value space (#158). Each is#allor a whitespace list of method tokens, but the admitted tokens depend on the component: an element'sfinalisextension/restrictionand itsblockaddssubstitution; a complexType'sfinal/blockareextension/restriction; a simpleType'sfinalislist/union/restriction. The tokens were parsed leniently (unknown tokens ignored,substitutionadmitted everywhere), sofinal="substitution"on an element, a misspelled or miscased token, or#allmixed with other tokens was accepted (the elemF/st_final families). The list is now validated against the component's exact set;finalDefault/blockDefault(the wider schema-level value space) and foreign attributes are left alone. XSTS: invalid schemas accepted 580 to 537, no other bucket raised (valid schemas rejected held at 72). -
A
simpleContentderivation's content is now checked for order and disallowed children (#158). Arestrictionis(annotation?, (simpleType?, facet*), (attribute | attributeGroup)*, anyAttribute?)and anextensionis(annotation?, (attribute | attributeGroup)*, anyAttribute?): simpleContent has no model group, an extension has no facets orsimpleType, and the attributes precede a single trailinganyAttribute. The membership table admitted the names but not the model, so a model group inside simpleContent, a facet in an extension, a facet before itssimpleType, an attribute afteranyAttribute, or a secondanyAttribute(the ctD family) was accepted. The derivation's children are matched against the context's ordered slot model, reusing the slot walk built for complexContent. XSTS: invalid schemas accepted 594 to 580, no other bucket raised (valid schemas rejected held at 72). -
Particle Valid (Restriction) now checks an element's type by derivation, not just its name (NameAndTypeOK, #158). When a restriction maps a same-named element onto a base element, the restricting type must be, or derive by restriction from, the base element's type; renaming
xs:stringtoxs:int, widening a concrete type toxs:anyType, or naming an unrelated type (a different user type, a wider ancestor, an absent/anyTypetype over a concrete base) was accepted because only the names were compared. Each element particle now carries its type's derivation identity (the resolved type name,anyTypefor an absent type, or nil for an inline anonymous type), namedsimpleTyperestriction derivations are recorded alongside complex ones, and the check walks the user restriction chain and then the built-in lattice to decide derivation by name; an ur-type base, a union base (membership is not a restriction chain), and an inline anonymous type are permitted, so no valid restriction is rejected. XSTS: invalid schemas accepted 621 to 594, invalid instances accepted 165 to 160 (more invalid schemas rejected up front), no bucket raised (valid schemas rejected held at 72). -
An element/attribute
default/fixedvalue must be a valid value of itstype(Attribute/Element Locally Valid, XSD 1.0, #158). Afixed="abc"on anxs:integeror adefault="1 2"on anxs:intwas accepted because the value constraint was never checked at compile time. The value is now validated against its type when the type resolves to a built-in datatype (namespace-aware: only atypein the XSD namespace), normalising whitespace per the type as XSD requires (sodefault=" 5 "on an integer is valid); a named or inline user type is left to its own rules, so no valid value constraint is rejected. XSTS: invalid schemas accepted 638 to 621, no other bucket raised (valid schemas rejected held at 72). -
A constraining facet must apply to its simple type's variety (XSD 1.0 Part 2, #158). A
listmay be constrained only bylength/minLength/maxLength,pattern,enumeration, andwhiteSpace; auniononly bypatternandenumeration. A value-bound facet (maxInclusive,totalDigits, ...) constrains an atomic value space, so restricting a list (list of integer) bymaxInclusivewas wrongly accepted. The base's variety is resolved from the raw tree (the check runs before the types are compiled), namespace-aware so a user type that shares a built-in's local name does not flip the variety: a base in the XSD namespace is a built-in (only the list datatypes are list-variety), a base in the schema's own target namespace resolves to a declared global, and an external or unresolvable base is skipped, so no valid restriction is rejected. XSTS: invalid schemas accepted 664 to 638, no other bucket raised (valid schemas rejected held at 72). -
A complexType's shorthand content (without
simpleContent/complexContent) is now checked for order, not just cardinality (#158). Its model is(annotation?, (group | all | choice | sequence)?, (attribute | attributeGroup)*, anyAttribute?): an optional model group, then attributes, then an optionalanyAttribute. The existing content-shape check caught the wrong number of model groups and the exclusivity of a content spec, but not the order, so a model group after an attribute, an attribute afteranyAttribute, or a secondanyAttribute(the ctB family) was accepted. The complexType's direct children, excludingannotationand the content specs, are matched against the same ordered slot model used for acomplexContentderivation. XSTS: invalid schemas accepted 687 to 664, no other bucket raised (valid schemas rejected held at 72). -
An
any/anyAttributewildcard'snamespaceconstraint is now validated at compile time (#158). Its value space is('##any' | '##other')standing alone, or a list of namespace URIs and the##targetNamespace/##localtokens; the wildcard parser mapped any unrecognised token to a literal URI, so a misspelled token (##all,##target), an##any/##otherplaced inside a list (##any ##other), or any other##-prefixed token was accepted. The unprefixednamespacevalue is now tokenised and checked against that grammar, while##any/##otherare admitted only as the whole value; a prefixed (foreign) attribute that merely shares the local name is the author's own and is not checked, so no valid schema is rejected. XSTS: invalid schemas accepted 736 to 687, no other bucket raised (valid schemas rejected held at 72). -
An identity constraint's
selector/fieldxpathis now validated against the XSD 1.0 restricted-XPath subset at compile time (#158). That subset (Part 1, 3.11.6) is far narrower than full XPath: a path is an optional leading.//then/-separated steps, a step is., a name test (QName,*, orNCName:*), or achild::axis, and afieldpath may end in an attribute step (@xorattribute::x); there are no predicates ([...]), no other axes (self::,descendant::), no absolute or interior//. The xpath was stored raw and compiled lazily withtry?, so a malformed one (a predicate, a bare/, an empty union branch, a spaced name-test colon) was silently accepted. It is now lexed into a whitespace-tolerant token stream and parsed against the grammar, so whitespace between tokens is allowed (child :: a,. //.) while//must be adjacent and a name test's own:may not be spaced; the name-test lexer uses the XMLNameCharset, so a non-ASCII NCName is accepted. XSTS: invalid schemas accepted 785 to 736, no other bucket raised (valid schemas rejected held at 72). -
A
complexContentderivation's content is now checked for order and cardinality, not just child membership (#158). ArestrictionorextensioninsidecomplexContenthas the content model(annotation?, (group | all | choice | sequence)?, (attribute | attributeGroup)*, anyAttribute?): an optional model group, then any number ofattribute/attributeGroup, then an optionalanyAttribute. TheallowedChildrenmembership table admitted the right child names but not their sequence or count, so a second model group (sequencethensequence), a model group after an attribute, an attribute afteranyAttribute, or a secondanyAttributewas accepted. The children are matched against an ordered slot model (one model group, unbounded attributes, oneanyAttribute) by a forward-only walk; the check runs only in thecomplexContentcontext, where a model group is part of the model, so asimpleContentderivation (facets, no model group) keeps its own rules and no valid schema is rejected. XSTS: invalid schemas accepted 864 to 785, no other bucket raised (valid schemas rejected held at 72). -
Unique Particle Attribution (
cos-nonambig, the content-model determinism constraint) is now enforced at compile time (#156, #157), the keystone of the remaining schema-validity tail. A content model must be deterministic so each element in an instance is attributable to a single particle without lookahead; an ambiguous model (<choice>of two same-name elements, an optional or repeating element followed by a distinct same-name element, two same-name members of anall, an element competing with a wildcard that admits it) was accepted. The check runs a Glushkov position automaton over a literal, QName-resolved view of the raw schema tree built specifically for this rule, independent of the instance-validation content model: arefis one position carrying its prefix-resolved QName (not the blanket target namespace), and substitution groups are not expanded, which removes the two false-overlap sources (cross-namespace refs, expanded substitution members) that a check over the compiled model produced. Determinism is per particle, the W3C rule's defining property: a particle competing with a repetition of itself is never a violation, so a fixed count{n,n}unrolls into copies in sequence (b{2,2}, bstays the deterministicb, b, b) while a true range or unbounded repeat uses a self-loop, and only two distinct source particles with overlapping labels conflict. The overlap test is QName-only (not substitution-group-aware), an accepted under-rejection that can never over-reject. XSTS: invalid schemas accepted 884 to 864, no other bucket raised (valid schemas rejected held at 72). -
A
keyref'srefermust name akeyoruniqueconstraint (#147). Areferpointing at a non-existent (or non-key/unique) constraint was accepted; it is now resolved against the document'skey/uniquenames, with a forwardrefer(textually before the key) handled by collecting all names first. The check is skipped when the document pulls in external definitions throughimport/include/redefine, which the default compile does not load. XSTS: invalid schemas accepted 891 to 884, no other bucket raised. -
Component-name uniqueness is now enforced (#147). Within a schema, global type names (
simpleTypeandcomplexTypeshare one symbol space), global element names, global attribute names, named model-group names, and named attribute-group names must each be unique, and identity-constraint names must be unique across the document; a duplicate was accepted, with the later definition silently overwriting the earlier. Only the document's own globals are examined (anxs:redefine's same-named type is not a clash), and the symbol spaces are kept separate (a type and an element may share a name). XSTS: invalid schemas accepted 909 to 891, no other bucket raised. -
Element Declarations Consistent (
cos-element-consistent) is now enforced (#147). Within one complex type's content model, two element declarations with the same name must have the same type; two same-name elements with different types (e.g.aasxs:stringand asxs:integer) were accepted. The check walks the content model (through model groups and content-derivation wrappers) but not into an element's own nested type, so a reused name in a nested anonymous type, a separate content model, is not flagged. Modeled on Xerces (libxml2 only partially enforces this); substitution-group expansion is not folded in (an accepted under-rejection). XSTS: invalid schemas accepted 925 to 909, no other bucket raised. -
Schema-vocabulary attribute applicability is now enforced at compile time (#147), the attribute sibling of the content-model table. Each XSD component admits a fixed set of attributes (the schema for schemas); an unprefixed attribute outside that set (a typo like
nullable, a strayfoo,nameon asequence) is now rejected, andrefmay not co-occur withnameortype. Foreign (prefixed) attributes and namespace declarations are not the component's own and are not checked, and the element/attribute sets are the permissive union of global and local use, so no valid schema is rejected. XSTS: invalid schemas accepted 956 to 925, no other bucket raised. -
A named model-group definition (
xs:group name="...") must contain exactly one ofall/choice/sequence(#147). XSD 1.0 gives a group's content as(annotation?, (all | choice | sequence)), but theallowedChildrenset-membership check did not enforce the cardinality, so a group with two compositors or with none was accepted. Agroupreference (noname) is excluded. XSTS: invalid schemas accepted 977 to 956, no other bucket raised. -
A particle whose
minOccursexceeds itsmaxOccursis now rejected at compile time (#147). An occurrence range likeminOccurs="5" maxOccurs="2"is empty and illegal, but was accepted; the two values are compared by canonical magnitude (independent of machine-integer range), withunboundednever exceeded and a single-point range (minOccurs="2" maxOccurs="2") allowed. XSTS: invalid schemas accepted 997 to 977, no other bucket raised. -
A complexType's content-model shape is now enforced at compile time (#147). XSD 1.0 allows a complexType's content to be
simpleContent, orcomplexContent, or a model group followed by attributes, but never a mix: so at most one ofsimpleContent/complexContent, neither beside a model group or a direct attribute (those belong inside the derivation), and at most one model group. TheallowedChildrenset-membership check did not catch these combinations, so a malformed complexType (twosimpleContent,simpleContentplus asequence, two model groups) was accepted. Only the complexType's direct children are examined, so attributes legitimately nested inside an extension/restriction are not flagged. XSTS: invalid schemas accepted 1051 to 997, no other bucket raised. -
Unresolvable references are now rejected at compile time (#147). Every QName a schema names (a
type/base/itemType/memberTypestype, anelement/attribute/group/attributeGroupref, an elementsubstitutionGroup) must resolve to a declared component or a built-in; an undeclared reference was previously accepted and only surfaced (if at all) lazily during instance validation. The reference is matched by local name with whitespace trimmed, as the rest of the compiler resolves it. The check is skipped when the document pulls in external definitions throughimport/include/redefine, which the default compile does not load, so a reference into them is never flagged. XSTS: invalid schemas accepted 1092 to 1051, no other bucket raised. -
patternfacet regular expressions are now validated at compile time (#147). Eachpatternvalue is compiled when the schema is; a structurally malformed one (unbalanced parentheses((a), a quantifier with nothing to repeat?a) is rejected, where before it was only compiled lazily at instance time (try?) and so silently ignored. The check is conservative: a construct the engine merely does not support on an otherwise-valid pattern (the empty pattern, an untabulated\p{Is...}block such as a surrogate or historic block, the lenient{,m}quantifier) is tolerated, so no valid schema is rejected. XSTS: invalid schemas accepted 1129 to 1092, no other bucket raised. -
Schema-vocabulary name and reference attribute values are now checked for lexical validity at compile time (#147). A component's
namemust be a valid NCName (name="",name="123",name="a:b"are rejected), and the reference attributestype/base/ref/itemType/refer/substitutionGroupmust be lexical QNames (type=":_",type="a:b:c"rejected), withmemberTypesa list of QNames; these were unchecked and the schema accepted. Only the lexical form is checked (a QName's prefix is resolved elsewhere), so no valid schema is rejected. XSTS: invalid schemas accepted 1190 to 1129, no other bucket raised. -
Schema-vocabulary attribute values are now validated at compile time (#147), the attribute-model sibling of the structural content-model check. Enumerated attributes (
form/elementFormDefault/attributeFormDefaulttoqualified/unqualified,usetooptional/prohibited/required,processContentstoskip/lax/strict,mixed/abstract/nillableto a boolean) andminOccurs/maxOccurs(anonNegativeInteger,maxOccursalsounbounded) are checked against their fixed value space; an out-of-space value (use="foo",mixed="yes",minOccurs="-1") was previously ignored and the schema accepted. Only unprefixed attributes on XSD-namespace elements are checked, so a foreign attribute is never flagged. XSTS: invalid schemas accepted 1313 to 1190, no other bucket raised. -
Schema documents are now structurally validated against the schema-for-schemas content model (#147), the general mechanism for the structural tail of the schema-validity gap. A data-driven table of the XSD 1.0 child content model rejects, at compile time, any component child the model does not admit, more than one
annotation(or anannotationthat is not first, except onschema/redefine), and an identity constraint missing its selector or field. The check walks only XSD-namespace elements and skipsxs:appinfo/xs:documentationforeign content, so no valid schema is rejected;restriction/extensionuse the permissive union of their contexts (never over-rejecting). One table covers every component rather than a rule hand-coded per element. XSTS: invalid schemas accepted 1688 to 1313 (-375), no other bucket raised. -
The
idattribute on XSD components is now validated at compile time (#147). It is of typexs:ID, so each value must be a valid NCName and unique within the schema document; the values were never checked, so a malformed (id="",id="123") or duplicatedidleft the schema wrongly accepted. The schema-document tree is walked, skippingxs:appinfo/xs:documentationforeign content (whoseidattributes are notxs:ID) and treating only the unprefixedid(not a foreignxml:id/pre:id) as the component identifier, so no valid schema is rejected. XSTS: invalid schemas accepted 1789 to 1688, no other bucket raised. -
Value-bound facet definitions are now validated at compile time (#147), continuing the facet-validity work. Per XSD Part 2 Datatypes 4.3, each
minInclusive/maxInclusive/minExclusive/maxExclusiveandenumerationvalue must be a valid value of the base type;minInclusiveexcludesminExclusiveandmaxInclusiveexcludesmaxExclusive; and the lower bound may not exceed the upper (nor equal it when either side is exclusive, an empty value space), compared in the base primitive's value space. XSTS: invalid schemas accepted 2082 to 1789, no other bucket raised. -
xs:gMonthnow accepts the XSD 1.0 lexical form--MM--(the trailing--is optional, the later--MMerratum form still accepted), with an optional timezone (#147). Only--MMwas recognized, so validgMonthvalues and the schemas whose facets used them were wrongly rejected. Surfaced by the value-bound facet check above (which validates a bound value against its base type). XSTS: valid instances rejected 233 to 214; it also removed 26 false schema rejections the bound-facet check would otherwise have caused. -
Constraining-facet definitions are now validated at compile time (#147), the first cut at the schema-validity gap (invalid schemas the validator was silently accepting). Per XSD Part 2 Datatypes 4.3, each length-family facet value (
length,minLength,maxLength,totalDigits,fractionDigits) must be a validnonNegativeInteger(totalDigitsapositiveInteger),lengthmay not co-occur withminLength/maxLength, and the integer ranges must be ordered. Malformed values (length="","-1","a","1e2") were dropped by numeric parsing and the schema accepted; the definition is now read raw and rejected at compile time, with well-formedness checked independently of machine-integer range so a huge but valid value is not misreported. The findings travel through a shared diagnostics collector on the parsing context and join the model-level consistency findings. XSTS: invalid schemas accepted 2461 to 2082, with no change to any other bucket. -
A built-in datatype named by
xsi:type(xsd:int,xsd:decimal,xsd:string) is now resolved, gated by substitution validity (#147). The override resolved only against schema-declared types, so a built-inxsi:typewas reported "unknown" and the element wrongly rejected. The built-in is now resolved when it validly substitutes for the declared type per Schema-Validity Assessment: the ur-type (anyType/anySimpleType) admits any type, and a bare built-in declared type admits any built-in derived from it through the XSD Part 2 datatype lattice (newBuiltinType.derives(from:)). An invalid built-in substitution is a located error, not a silent acceptance, closing the gap the resolution exposed. XSTS: valid instances rejected 248 to 233 (with no regression in invalid instances accepted, which the two fixes below preserve). -
A simple-typed element now rejects non-schema-instance attributes (#147). A simple type declares no attribute uses, so an element whose type is simple, including one whose simple type comes from an
xsi:typeoverride and including anxsi:nilelement, may carry only thexsi:*and namespace attributes; any other attribute is a violation. The check was absent, so resolving a built-inxsi:typeto a simple type would otherwise have accepted stray attributes on it. -
Identity-constraint fields compare in their value space when the selected node carries an
xsi:type(#147).unique/key/keyreftuples compared lexically, so twoxsi:type="xsd:decimal"fields3.0and3, the same value, were treated as distinct and auniqueviolation was missed. Each field value now carries the simple type read from the node'sxsi:type; two values are equal in that type's value space when both share a built-in base (reusingSimpleType.valueMatches), otherwise by lexical form. -
Simple-content values preserve significant whitespace per the type's
whiteSpacefacet (#147).textContenttrimmed leading/trailing whitespace unconditionally before the facet ran, so anxs:string(whiteSpace="preserve") value that was whitespace-only or had significant edges (e.g. matching a\tpattern, or counted by alengthfacet) was wrongly emptied or shortened. Simple-content validation now reads the raw character content and lets the facet normalize it; the element-only/empty "no significant text" check still ignores indentation whitespace. XSTS: valid instances rejected 288 to 248.
Added
Schema.Document.validate(_:schemaLoader:)honors an instance'sxsi:schemaLocationandxsi:noNamespaceSchemaLocation(#147). Each referenced schema document is loaded through the supplied loader, compiled, and its global declarations merged in (the primary schema's own declarations win on conflict), so a strict or lax wildcard can resolve an element declared in another document. The plainvalidate(_:)is unchanged. XSTS: valid instances rejected 308 to 288 (the particle clusters whose wildcard-matched elements live in a separate, instance-referenced schema).
Fixed
- A
mixed="true"complex type with no content model now permits character data (#147). Such a type (only attributes, nosequence/choice/all) was compiled to the empty content type, which forbids text, so a mixed element carrying only text was rejected. It is now mixed content over an empty particle: text is allowed, child elements are still not. XSTS: valid instances rejected 311 to 308. <xs:attribute ref="...">references to global attribute declarations are now resolved (#147). A reference has noname, so it was dropped and the referenced attribute looked undeclared on the instance; nested attribute-group references were already flattened, but the per-attribute reference was not. The reference now resolves to the global declaration's type, takinguse/default/fixedfrom the reference site. XSTS: valid instances rejected 338 to 311.- Document-scoped
xs:ID/xs:IDREFvalidation is now enforced (#147).xs:ID,xs:IDREF, andxs:IDREFSwere checked only lexically (as NCNames); the document-wide constraints, that every ID is unique and every IDREF/IDREFS item matches some ID, were not. The tree validator now records ID and reference values during its typed walk (elements and attributes) and reports duplicate IDs and unresolved references once the whole document is seen, so a forward reference resolves against a later ID. XSTS: invalid instances accepted 179 to 166. - The regex
\wand\Wescapes now follow the XSD definition (#147).\wwas the Perl class (alphabetic, decimal, underscore); XSD defines it as[#x0000-#x10FFFF]-[\p{P}\p{Z}\p{C}], so symbols and marks are word characters and the connector_(Pc) is not.\Wcorrespondingly matched symbols it should not. XSTS: invalid instances accepted 231 to 210. (Internal regex fixtures that assumed_was a word character were corrected.) block="substitution"on a substitution-group head is now enforced (#147). The substitution filter only dropped members whose type derived from the head's type by a blocked method (extension/restriction); thesubstitutionkeyword, which forbids substitution-group substitution outright regardless of the member's type, was ignored, so a same-type member still substituted under a substitution block. XSTS: invalid instances accepted 238 to 231, valid schemas rejected 75 to 74.- Element
defaultandfixedvalues are honored on empty elements (#147). An empty element with a value constraint takes that value, so the empty content is valid even when the type would reject the empty string; the constraint value (not the empty string) is what is validated against the type, including anyxsi:typeoverride. A restriction'senumerationnow also replaces the inherited enumeration rather than unioning with it (the new set is the value space;patternstill accumulates and is ANDed), which a value-constraint case underxsi:typeexposed. XSTS: valid instances rejected 395 to 369, invalid instances accepted 240 to 238. - Element-level
blockis now enforced againstxsi:typesubstitution (#147). Theblockon an element declaration (distinct fromblockon its type) was parsed but never threaded to the instance validator, so a document substituting a blocked-derivation type viaxsi:typewas wrongly accepted. The element block is now carried through toblockedSubstitutionError, which also resolves the element-ref type indirection (element:footo its concrete type) so the check fires for referenced children, not only the root. XSTS: invalid instances accepted 261 to 240. - The regex engine now recognizes the full XSD
\p{Is<Block>}Unicode block set (#147). Only about fifteen blocks were tabulated, so a pattern over any other block (\p{IsArmenian},\p{IsTamil},\p{IsHangulJamo}, and so on) failed to compile and rejected valid instances. The table now carries the Unicode 3.1 blocks the XSD regular-expression appendix fixes (surrogate blocks omitted, since surrogates are not scalar values); each range is verified bidirectionally by the XSTS corpus (valid in-block instances pass, invalid out-of-block instances still fail). XSTS: valid instances rejected 502 to 395. - Length facets on
xs:QNameno longer reject valid values (#147). XSD 1.0 Datatypes 4.3.1 does not define a unit of length for QName (only string, anyURI, binary, and list), solength/minLength/maxLengthon QName are implementation-defined; like Xerces and the XSTS NIST set we treat them as non-constraining rather than counting characters. Net XSTS: valid instances rejected 547 to 502 (36+ NIST false rejections fixed); 4 Microsoft cases that read QName length as a character count remain in invalid instances accepted (257 to 261) and are documented as a deliberate spec-ambiguity interpretation in docs/xsts-deviations.md. - Ordering facets on
xs:durationare now enforced (#147).minInclusive,maxInclusive,minExclusive, andmaxExclusiveon duration were ignored because duration has no total order in the generic comparator. ADurationValuenow implements the XSD 1.0 Datatypes 3.2.6.2 partial order (compare by adding to the four reference dateTimes); a value incomparable to the bound (such asP1MagainstP30D) is outside the range and rejected. XSTS: invalid instances accepted 355 to 257. - Pattern and enumeration facets on a union type are now enforced (#147). A union admits a value valid for any member, but
patternandenumeration(the only constraining facets XSD allows on a union) had been ignored, so a member-valid value outside the enumeration or failing the pattern was wrongly accepted. Enumeration compares in the value space of the admitting member (so01equals an enumerated1for an integer member), matching the atomic path. XSTS #147: invalid instances accepted 555 to 355. - Length facets on the built-in list datatypes now count list items, not characters (#146). A restriction of
NMTOKENS,IDREFS, orENTITIESresolved its base through neither theBuiltinTypetable (which has only the singularNMTOKEN) nor the user-type table, so it collapsed to an atomic string andlength/minLength/maxLengthmeasured characters, rejecting valid instances. The restriction base now resolves throughsimpleTypeReference, preserving the list variety. Closes the XSTS NISTlist-NMTOKENS/IDREFS/ENTITIESfacet clusters: valid instances rejected 604 to 547, invalid instances accepted 582 to 555. - Three crash and hang classes uncovered by running the full XSTS archive to completion (#129). (1) Nested occurrence explosion: the content-model NFA capped each particle's
maxOccursunroll at 16384 but not the total state count, so nested high-maxOccursparticles multiplied (a sequence of 16384 holding a choice of 16384 is 2.7e8 states) and OOM-killed the process at 8 GB on the Microsoft Particles set; the builder now also caps total states (2^20), degrading further repetition to star. The regex{n,m}quantifier builder had the same unbounded unroll and the same cap is applied. (2) Quadratic content-model validation:sequenceStructureErrorscalledfollow(after: prefix)once per child andfollowre-walked the whole prefix from the start each time, so a content model over many children spun at 100% CPU; matching is now incremental (one active state-set advanced per child), turning O(children squared) into O(children). (3) Character-class subtraction trap: the regex pattern[abcd-[d]](XSD class subtraction) was parsed as adto[range, and sinced(100) exceeds[(91) the reversedClosedRangetrapped (SIGBUS);-[now always opens a subtraction, and any reversed range (such as[z-a]) is a clean rejection rather than a trap.
Changed
- Scanner-side validity findings are now located structured values (validation-framework audit follow-up):
DocumentType.validityFindingscarriesValidityFindingvalues (reason plus the declared name they are about) instead of strings,DTDSchema.declarationErrorsholds readyValidationErrors, and every declaration-level finding renders at its declaration's coding path (x,r/@a) instead of "at root of document", matching the XSD consistency-errors precedent. Finding texts are unchanged.
Added
- W3C XSTS runner, first measurement (#129, #122): the W3C XML Schema Test Suite (2006-11-06 release, ~13k test groups across NIST, Sun, Microsoft, and Boeing contributions; W3C Document License, never vendored) runs opt-in via XSTS_ROOT: every schemaTest must compile when expected valid and be rejected when expected invalid, and every instanceTest validates against its group's schema with the expected outcome; hrefs resolve against the referencing file and include/import locations from disk relative to the schema. The four disagreement counts are asserted exactly; the first measurement landed 2026-06-13 and is baselined at 75 valid schemas rejected, 2467 invalid schemas accepted, 604 valid instances rejected, and 582 invalid instances accepted across 14383 groups, with per-case deviations written to /tmp/xsts-failures.txt for the burn-down. Building the runner immediately caught a denial-of-service bug: a schema with maxOccurs in the trillions (groupF009v) OOM-killed the process because the content-model automaton unrolled occurrence counts literally; unrolling is now capped at 16384 with larger repetitions treated as unbounded, the libxml2 posture. The roadmap mermaid records the closed behavioral-parity set (#141-#144, collation) and the #139 performance epic.
Added
- The last parity items, to one hundred percent (#143, #144, #130): XPath gains a configurable protective evaluation budget (
PureXML.XPath.Budget, with alibxml2Compatiblepreset matching its fixed 10M node-set cap), threaded through query evaluation and thrown as a locatedQueryError.budgetExceededinstead of libxml2's silent refusal, default unbounded; xsl:sort honorslang(an attribute value template) with tailored alphabet orders for Polish and Russian derived from the conformance golds (diacritics interleaved, Cyrillic Yo after Ye), case-folded, falling back to the default order for untailored languages; and the WASM gate executes the full test suite under wasmtime on an actual WASI runtime (build-only with a notice when wasmtime is absent), wired into CI. The first WASI run immediately caught a real wasm32 trap: format-number's digit extraction converted through Int, which is 32 bits on wasm32 and trapped at 2^31 despite the 2^53 guard; the internals are now explicitly Int64 and the xsl:number bound is platform-correct (Double(Int.max)). Xalan baseline 127 to 125.
Added
- Public streaming surface (#142) and a public-API test target:
EventReader.next(), both initializers, and theEventalias are public, so external consumers can pull events with bounded memory over arbitrarily large inputs (an internal typealias in a split file had been shadowing the publicParsing.Event, narrowingnext()to internal; the benchmark harness found it). A newPureXMLPublicAPITeststarget compiles against the public surface without@testable, so any public facade over internal members fails CI; it pins parse/query/serialize, streaming pull, and the chunked pull source.
Fixed
- XPath document-order sorting is benchmarked at scale across generated (4/42/210 MB) and real corpora (NASA ADC 25 MB, SwissProt 115 MB), all linear: the quadratic fix's own first attempt (an eager sibling table built per predicate evaluation) was caught by sampling the live run and replaced with a lazy table (single-node sets never touch order keys; a parent's table builds on its second lookup). XPath at 42 MB drops 27.1 s to 1.57 s (ratio 190x to 11.7x); at 210 MB PureXML completes in 9.4 s the query libxml2 refuses at its 10M node-set cap (the configurable protective budget is #143). The benchmark harness gains a size series, a CORPUS mode for real-world files, a SAX stage, and a refusal-aware comparison table.
- XPath document-order sorting is no longer quadratic on flat fan-outs (#139, found by the large-corpus benchmark): document-order keys looked up each node's index by scanning its parent's children, so sorting a node-set drawn from one wide element (the 42 MB corpus: 200k siblings) cost 27 seconds. A per-operation sibling-index cache enumerates each distinct parent once, making key computation linear in the tree.
Changed
- Byte-backed Reader, performance passes 2 and 3 (#139): String input now copies its UTF-8 once into owned storage (an unsafe pointer encapsulated in a class lifetime, never escaping the parser) and scalars decode straight off the pointer, eliminating the per-character closure chain; ASCII literals match and consume at the byte level; and plain-ASCII character data, the bulk of any document, scans as a single byte run with one String built per run. Cumulative parse time on the 4.2 MB benchmark corpus drops 32 percent (0.49 s to 0.34 s, libxml2 ratio 30x to 20x). The W3C conformance suite caught one boundary in the run scanner (a run starting with '>' could swallow the "]]>" check after the character path consumed "]]"; ibm14n01); the run scanner now leaves a leading '>' to the character path, with the case pinned as a unit regression. All suites (W3C xmlconf, RELAX NG, xalan), Linux, and WASM verified.
- First measured performance pass (#139): Reader.matches() no longer materializes an Array of the literal per call (it runs on every scan decision and was the hottest comparison in the parse profile); parse time on the 4.2 MB benchmark corpus drops 16 percent (0.49 s to 0.41 s, libxml2 ratio 30x to 26x). The benchmark harness, baseline, profile findings, and the optimization log live in docs/benchmarks.md.
Fixed
- Stylesheet composition cycles no longer crash (#130 critic loop): an include or import cycle (a.xsl includes b.xsl includes a.xsl) recursed without bound and killed the host with a stack overflow, a denial of service reachable from stylesheet data. The compile chain now tracks the active load chain by resolved href and drops re-entry, while diamond composition (two units legitimately loading the same sheet) keeps loading both copies with their own import precedences, per 2.6.2. id() also gains a per-document index built on first use instead of re-walking the whole document per call, matching key(). Cycle and diamond regression tests added.
Added
- Top-level stylesheet parameters (#141), nineteenth xalan burn-down (#130):
PureXML.XSLT.transformacceptsparameters: [String: String], overridingxsl:paramglobals by name (xsl:variable globals are never overridable; the stylesheet records which names are parameters across imports and includes). The conformance runner honors the xalan harness contract, reading<case>.paramfiles; en alphabet sorting passes, and the CRLF param files exposed a Swift grapheme gotcha (CRLF is one Character, so splitting on "\n" never splits; the runner splits on newline graphemes). Baseline 128 to 127; Polish/Russian alphabet tailoring stays baselined as the locale class. - Space-ignorable sort collation, eighteenth xalan burn-down (#130): xsl:sort's default text order now treats spaces as ignorable at the primary level, breaking ties by later-first-space (the Java Collator semantics the conformance golds encode), derived from the gold orderings rather than delegated to a locale library. Baseline 129 to 128.
- DTD-typed id() and node-kind generate-id(), seventeenth xalan burn-down (#130): the XSLT layer's id() now follows the XPath definition exactly, resolving only attributes the source document's DTD declares as type ID (internal or external subset, parsed through the transform's entity resolver) and yielding the empty set without a DTD, replacing the name-based heuristic for transforms; and generate-id() produces non-empty, distinct identifiers for attribute and namespace nodes through their identity hash instead of returning the empty string. Two unit tests pin both. Baseline 131 to 129.
- HTML 4 serialization corners, sixteenth xalan burn-down (#130):
basefontandisindexjoin the void-element set (no end tags), and the html method leaves an ampersand unescaped when a brace follows, the historical&{...}attribute convention of 16.2. Baseline 134 to 131 (output 20 to 17). - Globals, prefix wildcards, fifteenth xalan burn-down (#130): a top-level variable now evaluates with every previously evaluated global in scope, so a variable whose body references an earlier top-level param (directly or via call-template) resolves; same-name globals resolve by import precedence (folds happen in precedence order and the last same-name entry wins, so a later sibling import beats an earlier one and the importing sheet beats both); the stylesheet fold uniformly favors later contributions for attribute-set definitions, decimal formats, namespace aliases, and output settings, matching the post-order precedence model; and the XPath compiler accepts the
NCName:*name-test form, matching any name in the prefix's namespace (bound by URI, falling back to the in-document prefix string) at its -0.25 default priority, so@ped:*outranks@*. Three unit tests pin the behaviors. Baseline 144 to 134 (variable 10 to 5, conflictres 8 to 5). - Import precedence and relative composition, fourteenth xalan burn-down (#130): import precedences are assigned in post-order over the import tree, so sibling imports are distinct (later higher) and every unit sits above its own imports; apply-imports searches only the current template's stylesheet's own import subtree per 5.6 (templates carry their unit's precedence range), falling back to the built-in rule when that unit imports nothing; an included stylesheet's declarations join the including unit at its precedence while its imports join the import list; and include/import hrefs resolve against the including stylesheet's own URI, so nested relative chains load correctly. Also fixed along the way: an included document's tree was only weakly referenced once flattened, so its ancestor chain (xmlns declarations, exclusions in scope) could disappear before its templates were parsed. Two unit tests pin apply-imports scoping and relative chains. Baseline 159 to 144 (reluri 11 to 3, impincl 9 to 2).
- Attribute sets, copied namespaces, html booleans, loader entities, thirteenth xalan burn-down (#130): same-name
xsl:attribute-setdeclarations merge as ordered definitions per 7.1.4 (each expands its used sets before its own attributes, a later definition's attributes winning);xsl:copyof an element carries the source element's in-scope namespace nodes per 7.5 (the fixup pass drops result-redundant ones); the namespace fixup lets a carried attribute prefix shadow an inherited binding locally (matching the source document's shape) unless the element's own name relies on it; the html method minimizes boolean attributes whose value repeats their name (CHECKED="CHECKED" serializes as CHECKED); and external DTDs and entities referenced by the stylesheet or source now resolve through the transform's document loader, the same channel as document(). Four unit tests pin the new behaviors. Baseline 174 to 159 (attribset 13 to 4, copy 12 to 7).
Fixed
- format-number and xsl:number no longer trap on extreme values (#130 critic loop): format-number of a value at or beyond 2^53 rendered through an unguarded Double-to-Int conversion, a fatal trap reachable from user data (format-number(99999999999999999999 * 99999999999999999999, '#,##0') crashed the host); a fraction picture with more than 15 places overflowed the scale conversion the same way; and xsl:number value= beyond 2^53 trapped identically. Digit extraction now clamps fraction places to 15 (the 2^53 exactness bound), pads back to the picture's requested minimum, renders out-of-range integers through the exponent expander (digits stay exact), and xsl:number falls back to plain XPath number rendering outside [1, 2^53). Regression tests cover all three paths.
Added
-
Critic-loop hardening of the xalan burn-down (#130): raw-output marker handling is now gated on the stylesheet actually mentioning
disable-output-escaping, so a source document that happens to contain the private-use marker characters U+E000/U+E001 passes through every transform untouched (previously they were stripped, and text between them unescaped, unconditionally), and the text output method only removes markers without "unescaping" content that was never escaped; the pattern-matching function table is built once per transform instead of once per (template, node) membership test; and the d-o-e, CDATA "]]>" splitting, simplified-syntax, html default-method/META, per-document key(), and attribute-axes behaviors gain focused unit tests that run in CI without the opt-in corpus (eight new tests). -
Following/preceding from attribute nodes, twelfth xalan burn-down (#130): the following and preceding axes now work from attribute and namespace starts, anchored at the owner element per the XPath 1.0 document order (an attribute follows its element and precedes its children, and has no descendants of its own). Baseline 182 to 174 (axes 16 to 8).
-
Keys per document, in document order, eleventh xalan burn-down (#130):
key()resolves its index against the current node's own document (built lazily per document root, so keys work inside document() trees), a node-setuseexpression indexes the matched node under every member's string value, and key() results return in document order regardless of the lookup value order. Baseline 187 to 182. -
Sort keys in context and cross-document template matching, tenth xalan burn-down (#130): xsl:sort keys are computed once per node with the node's own evaluation context (its position and size in the selection, the caller's variables, functions, and namespace bindings), so position()-based and variable-referencing keys work, and NaN keys order before every number under data-type="number" (the Xalan order); match patterns evaluate over the matched node's own document rather than the transform source, so templates match nodes loaded through document() (the match cache keys include the document identity). Baseline 204 to 187 (sort 18 to 11, mdocs 14 to 6).
-
Prolog and CDATA conformance, ninth xalan burn-down (#130): the XML declaration precedes the document type declaration; a public-only doctype is emitted by the html method (16.2) and ignored by the xml method, which requires
doctype-system(16.1); the simplified stylesheet syntax of 2.3 (a literal result element carryingxsl:version) compiles as a single match="/" template; and cdata-section-elements text containing "]]>" splits across section boundaries so no section holds the terminator. Baseline 218 to 204 (output 34 to 20). -
Stylesheet namespace resolution, eighth xalan burn-down (#130): a template carries the xmlns bindings in scope at its stylesheet element, and they flow into its match pattern (the match cache keys on pattern plus bindings) and every expression evaluated while it is instantiated, so
match="baz2:doc"andselect="local-name(baz2:b)"resolve the prefix against the stylesheet and match by namespace URI; with bindings supplied the XPath 1.0 rule applies exactly, an unprefixed name test selecting only the null namespace. The instruction-scope namespace capture includes the default declaration, soxsl:elementhonors an in-scopexmlns="uri"for its unprefixed created names, an in-scopexmlns=""keeps them namespace-free (undeclared in the result when an inherited default would capture them), and an unusable created name (:foo) drops the wrapper and keeps the content, the Xalan recovery. Baseline 263 to 218 (namespace 41 to 13, axes 16, sort 18, output unchanged at 34, the rest spread). -
Keys, ids, and doctyped sources, seventh xalan burn-down (#130):
key()anddocument()read the string value of attribute and namespace node arguments instead of dropping them (the Muenchian grouping idiom works); match patterns evaluate with the XSLT function table, sokey('k', 'v')andid('x')patterns match (a key/id pattern branch is also no longer prefixed with//); and the XSLT entry points parse sources, stylesheets, anddocument()loads with doctypes allowed, which the conformance corpora use throughout. Baseline 292 to 263 (idkey 39 to 16, the rest spread). -
Templates over attribute and namespace nodes, sixth xalan burn-down (#130): xsl:apply-templates and xsl:for-each now keep attribute and namespace nodes in their selections instead of dropping them at the tree-node conversion. The evaluation context carries the non-tree current node (its owner element backs the tree-only machinery), XPath evaluation and current() start from it, match patterns select attribute nodes (the match cache keeps attribute identities, owner plus name, alongside tree identities), the built-in rules copy an attribute's or namespace's value as text, xsl:copy copies the focused attribute or namespace declaration itself, and an attribute result arriving after element content is ignored (the XSLT recovery). Sorting and apply-imports work over the generalized node kind. Baseline 352 to 292 (axes 35 to 16, copy 29 to 16, attribset 18 to 13, the rest spread).
-
Output methods in depth, fifth xalan burn-down (#130):
disable-output-escapingon xsl:value-of and xsl:text (raw text travels bracketed by private-use sentinels and the serializer's escaping is undone after the fact; a string extracted from a result tree fragment loses the markers per 16.4, so the variable round-trip case re-escapes); the default output method is html when the first result element is named html with a null namespace (16.1); the xml method writes the XML declaration unlessomit-xml-declaration="yes"(the spec default, with the caller's emit options as fallback only on explicit stylesheet silence); the html method injectsMETA http-equiv="Content-Type"into head (16.2), keeps SCRIPT/STYLE raw and void elements end-tag-free case-insensitively, and escapes U+00A0-U+00FF to the HTML 4.01 Latin-1 entity names. Whitespace-only detection in stylesheet/source stripping now means XML whitespace only, so an NBSP-only text node survives. The conf comparison treats whitespace adjacent to tag boundaries as layout for non-canonical comparisons. Baseline 371 to 352 (output 49 to 30). -
Literal result element namespace copying, fourth xalan burn-down (#130): a literal result element now copies its in-scope namespace declarations to the result per 7.1.1, minus the XSLT namespace and the namespaces of the prefixes listed in
exclude-result-prefixes/extension-element-prefixesin scope (#defaultincluded); an aliased stylesheet namespace declares its result prefix and URI instead. The namespace-fixup pass drops any declaration that repeats an inherited binding, so the copied nodes do not redeclare on every descendant, and created names now reuse the stylesheet's own prefixes (anamespace:Attr1rather thanns0:Attr1).xsl:output indent="yes"switches to the corpus golds' Xalan indentation form: children on their own lines with no leading spaces. Two unit expectations updated to the spec behavior (a literal element under stylesheet-level declarations carries them; collapsed alias+result declarations). The xalan baseline drops from 446 to 371 (namespace 89 to 42, output 50 to 49 and others spread across categories). -
format-number picture parsing in full, third xalan burn-down (#130): the picture now parses JDK DecimalFormat-style into literal prefix/suffix affixes around the number part, the negative subpattern after the pattern separator (its affixes apply; without distinguishing ones the minus sign still prefixes, the Xalan resolution, which also covers colliding decimal/grouping separators by reading every occurrence as grouping), per-mille (with a custom symbol via the new
per-milleattribute) and percent as multipliers kept literally in the output, the grouping size taken from the last grouping separator's distance to the decimal point instead of a fixed 3, and the generic currency sign U+00A4 rendering as this processor's en-US$. Digit extraction is rewritten over one scaled rounding plus pure integer arithmetic, so 0.4812 renders 4812 rather than re-accumulating float error per digit. The xalan numberformat category drops 30 to 1 (the survivor is a namespace-copy case); baseline 475 to 446. -
Full xsl:number per XSLT 1.0 section 7.7, second xalan burn-down (#130):
levelsingle/multiple/any withcountandfromas real patterns matched through the transform's match cache (default count: same node type and expanded name; single finds the nearest matching ancestor-or-self and numbers it among matching preceding siblings, multiple numbers every matching ancestor-or-self outermost first, any counts matching nodes at or before this one in document order withfromas the boundary, always yielding a number so an unmatched count renders 0); thevalueexpression evaluates and rounds, bypassing formatting below one; and the full format-token engine (leading/trailing punctuation, alternating alphanumeric tokens and separators with last-token and last-separator reuse, zero-padded decimal widths,a/A/i/Iand the Greek alphabetic sequence,grouping-separator/grouping-sizedigit grouping). A single/multiple level whose count matches nothing renders as the empty string, punctuation included. The xalan baseline drops from 557 to 475 (numbering 84 to 7, the rest pattern-engine prefixed/key/id cases). -
document()is stable per transform (#130): the same URI reference now returns the identical document object across calls within one transform, per the XSLTdocument()definition, via a per-transformer document cache; previously each call re-parsed, so node identity, and with itgenerate-id()uniqueness against live nodes, could be violated when an address was reused (the idkey49 flake). -
XSLT namespaced name creation and result fixup, first xalan burn-down (#130): xsl:attribute and xsl:element honor the
namespaceattribute and resolve a prefixednameagainst the instruction's own in-scope stylesheet declarations (7.1.2/7.1.3, captured at parse time so AVT names resolve correctly); xsl:copy appliesuse-attribute-sets; and a namespace-fixup pass runs over the result tree before serialization, declaring every namespace a created element or attribute name carries, reusing an existing binding for the same URI or generatingns0,ns1, ... prefixes when the carried prefix is absent or taken, and undeclaring an inherited default namespace that would capture an unqualified name. The xalan baseline drops from 604 to 557 (attribset 52 to 20, namespace 123 to 92, axes 68 to 35); one namespace-alias unit test updated to expect the now-declared result namespace. -
XSLT 1.0 conformance runner over the Apache xalan-test corpus (#122, #130): the de facto OASIS-era suite (1690 gold-bearing cases across 36 categories) runs opt-in via XALAN_TS_ROOT, never vendored. Comparison is deliberately normalized: outputs that parse as XML compare by canonical form (Xalan's indentation is not normative), others with whitespace runs collapsed. First measurement: 1086 pass; the 604 known failures are carried exactly in Tests/Fixtures/xalan-baseline.txt, asserted in both directions (a fixed case must leave the file, a regression shows as a new failure). The top burn-down classes by count: namespace handling (123, led by namespaced xsl:attribute creation), xsl:number depth (85), axes corners (68), output/serialization options (55), attribute sets (52), id()/key() (41).
-
RELAX NG schema-correctness validation (#131): a schema document is now validated before any pattern interpretation, in three layers. The grammar check (section 3) rejects unknown or misplaced elements, illegal unqualified and RELAX NG-namespace attributes, wrong child arity, stray text, malformed or undeclared-prefix QNames, non-URI datatypeLibrary values (absolute, fragment-free, valid percent escapes), fragment-carrying hrefs, the 4.16 except restrictions (anyName/nsName inside excepts, propagated through choice), and the xmlns name prohibitions including the ns-attribute and except-mention forms. The compiler checks reject combine conflicts and double combine-less defines (4.17), unresolved refs and parentRef without a parent, startless grammars, include/externalRef self-reference, include overrides with nothing to override, non-grammar include targets (loaded documents are grammar-checked too), unknown datatypes, and params against the built-in library. The restrictions checker (4.19, 7.1-7.4) runs over the compiled algebra after 4.20/4.21 normalization (notAllowed/empty operands simplify away first; recursion detection deliberately precedes it): prohibited paths under attribute, oneOrMore-group, list, and in start; computable content types; duplicate attributes by representative-name overlap; infinite-name-class attributes outside oneOrMore; and the interleave text/element constraints. The spec suite's incorrect class drops from 213 compiling to 7, all pre-Fifth-Edition name-class cases (the xmltest-141 class, count asserted), with zero correct-schema or instance regressions. Two leniency tests updated to the spec behavior (double combine-less defines and unknown datatypes are schema errors).
-
RELAX NG QName values, instance level clean (#131):
<value type="QName">compiles to its (namespace, local-name) value pair via the schema's xmlns/ns scope, and the derivative engine threads the instance element's in-scope namespace bindings down to value comparison (in both the tree walk and the streaming state, whose frames now carry their bindings), so an instance QName resolves its prefix, or the default namespace when unprefixed, against the instance document itself; an unbound prefix matches nothing. The streaming path also gains the tree walk's empty-element rule. With this the spec suite's instance level is fully clean: valid 289/289 accepted and invalid 291/291 rejected; only the asserted schema-correctness class remains. -
RELAX NG grammar scoping and include depth, third burn-down (#131): each
grammarelement opens its own define scope (names are scope-mangled in the flat table, so nested grammars do not collide) andparentRefreaches the enclosing scope;includeis transitive (an included grammar's own includes merge first) and a define carried by the include element replaces the included grammar's same-name define (the 4.7 override);divwrappers flatten into their grammar while still contributing ns/datatypeLibrary inheritance (4.11);valuetext is kept untrimmed so whitespace-preserving datatypes compare exactly (6.2.9); anddataExceptrecurses through choice patterns. The spec suite is now clean except one class: valid 286/289 and invalid 287/291, with all six remaining instance cases the QName datatype (its value space needs prefix resolution in both the schema and instance contexts), plus the asserted schema-correctness item. -
RELAX NG namespaces and semantics, second burn-down (#131): the compiler now implements
nsinheritance and the QName form of names (4.9-4.10:nswalks ancestors and crosses include/externalRef document boundaries;name="eg:foo"and<name>eg:foo</name>resolve their prefix against the schema's in-scope xmlns declarations; thename=shorthand on an attribute stays namespace-less rather than inheriting), anincludebrings in the included grammar'sstartwith an include-level override honored (4.7), multiple<start>elements combine like defines and a single define or start may omitcombinewhile still merging by the named method (4.17),<data>supports<except>via a newdataExceptpattern in the derivative engine (4.12), an empty element matches a data/value pattern whose lexical space admits the empty string (Clark's empty-text rule), and a whitespace-only attribute value matches a pattern that matches empty. Spec-suite baselines tighten from valid 242/289 to 274/289 and invalid 278/291 to 279/291; the name-class and tree-helper sections split to their own files for the length caps. -
RELAX NG simplification, first burn-down (#131): foreign-namespace elements are stripped before pattern interpretation (4.1: only RELAX NG-namespace elements are schema content; a namespace-less element counts only inside a fully unqualified schema), name/type/combine attribute values are whitespace-trimmed (4.2), and include/externalRef hrefs resolve against the xml:base attributes in scope and the loaded document's own URI (4.5, reusing the RFC 3986 resolver), with the document base threaded through nested loads and visited-tracking keyed on resolved hrefs. Spec-suite baselines tighten: valid 242/289 accepted (from 233), invalid 278/291 rejected.
-
RELAX NG spec test suite runner (#122, #131): James Clark's spectest.xml (385 cases, distributed with jing-trang) runs via an opt-in RNG_TS_ROOT runner, never vendored; the manifest is parsed with PureXML itself, nested testSuite scopes thread their resource/dir definitions into the compiler's loader, and each case asserts compile/reject for the schema plus accept/reject per instance. First measurement, carried as exact baselines: every correct schema compiles and most instances behave (valid 233/289 accepted, invalid 277/291 rejected), while schema-correctness validation is absent (all 213 incorrect schemas compile, count asserted exactly). The burn-down classes are recorded in the runner: schema validation against the RELAX NG grammar and the 4.16-4.18 restrictions, foreign-namespace stripping (4.1), xml:base-aware include/externalRef resolution (4.5), and the section 6 compatibility datatypes.
-
Canonical XML 1.0 spec-example vectors (#122, #132): the worked examples of the C14N 1.0 Recommendation's section 3 run as input/expected-output tests (cited per example; used under the W3C Software and Document License), all six passing: top-level PIs/comments with and without comments mode, whitespace preservation, start/end tags with attribute sorting and DTD defaults, character modifications and references, entity expansion, and UTF-8 output from legacy-encoded input. The vectors exposed three real conformance fixes: top-level siblings in the canonical form are now separated by single line feeds; an empty default-namespace declaration renders only when it undeclares a rendered non-empty default (superfluous
xmlns=""is dropped, inclusive mode); and the text/attribute escape loops iterate Unicode scalars, so a carriage return that grapheme-clusters with a following line feed still escapes to
(the #135 class, surfacing in output rather than lexing). -
Scalar-level lexing (#135): the reader now buffers exactly one Unicode scalar per element (multi-scalar graphemes from any source are split, string input iterates scalars directly), so a combining mark directly after an ASCII delimiter can no longer merge with it into one Swift grapheme cluster and XML's scalar-defined name productions classify correctly: U+06D6 lexes as a Fifth Edition PI-target NameStartChar, ZWNJ/ZWJ attribute names parse, and U+309A markup from entity expansion forms a real element. The replacement-text, content-model, and ATTLIST sub-scanners and the entity-literal validator go scalar-level the same way, while grapheme clusters reassemble naturally wherever scanned text is appended to a String (asserted by test). This clears the last eduni baseline (now empty), grows the IBM pre-5e class from 279 to 300 exactly as the Fifth Edition prescribes, and, combined with the EDITION="1 2 3 4" exclusions the xmltest manifest itself carries for cases 140/141, empties the xmltest deviation baseline entirely: every xmlconf case applicable to a Fifth Edition, namespace-aware processor now passes.
-
Strict internal-subset profile (#128):
Limits(strictInternalSubset: true)holds the internal DTD subset to the letter of XML 1.0, rejecting conditional sections and parameter-entity references inside markup declarations (entity values, content models, ATTLIST bodies), both of which PureXML otherwise supports as features; DeclSep references between declarations and%name;inside quoted attribute defaults (AttValue, where%is literal) stay legal, and defaults are unchanged. The xmltest runner opts in, taking its baseline from 181/186 to 185/186 with one remaining documented deviation: case 141, which expects pre-Fifth-Edition name-character classes. -
Per-entity base-URI tracking (#138): a relative system identifier declared inside an external entity now resolves against that entity's own URI (RFC 3986, reusing the C14N resolver), so nested external entities find their siblings;
ExternalIDgains an optionalbaseand aresolvedSystemID, the DTD scanner threads the current entity's URI through external-subset and parameter-entity scans, and resolvers receive the already-merged identifier (raw identifiers unchanged when no base applies). Clears eduni errata-2e/E18; the eduni baseline now holds only the #135 grapheme-lexing class. -
Encoding-declaration contradictions are fatal (#137, 4.3.3): a document whose XML declaration names a 16- or 32-bit encoding over BOM-less 8-bit bytes is rejected; a BOM followed by a declaration outside its family (a UTF-8 BOM with
encoding='iso-8859-1', a UTF-16 BOM with an 8-bit name) is rejected, with the declaration read through the BOM's own byte pattern; and an external entity whose text declaration names a higher XML version than the document's is refused (errata E38), in both the external-subset and external-entity paths. The eduni not-wf baseline is now completely empty. -
Namespaces 1.0 constraints (#136): the namespace layer now enforces the binding rules as located parse errors: malformed qualified names (
foo:,:foo,a:b:c,xmlns:) are rejected; thexmlnsprefix may never be declared;xmlmay be bound only to its own namespace name, which no other prefix (nor the default) may take, likewise the xmlns namespace name; a prefix may not be undeclared (Namespaces 1.0); attributes must be distinct by expanded name (the same URI+local name through two prefixes is a duplicate); PI targets and entity/notation names are NCNames; and a namespace declaration whose attribute is DTD-declared with a tokenized type binds its normalized value (sourn:xandurn:xare one namespace name, eduni ns 012). Clears all 18 namespace cases from the eduni baseline, leaving only the four #137 encoding-mismatch cases; xmltest valid/sa/012 (an attribute literally named:) joins the documented deviations per the suite's own NAMESPACE='no' flag, mirrored by the eduni runner's new NAMESPACE filter. -
W3C xmlconf eduni sections (#121, #127): the Edinburgh errata suites (2e, 3e, and the 5th-edition 4e), Namespaces 1.0 with its errata, and the misc cases run via the XMLCONF_ROOT runner; XML 1.1 and Namespaces 1.1 are explicitly out of scope and EDITION-gated cases skip when they do not apply to the Fifth Edition. The burn-down fixed a real spec bug and several errata behaviors: attribute values now get the 3.3.3 CDATA normalization (literal whitespace becomes a space before reference decoding, so character-referenced whitespace survives, fixing errata E36 and E20 semantics) and tokenized normalization collapses only SPACE characters; an undeclared general entity in content or an attribute value is a deferred validity finding instead of a fatal error when unread external declarations might supply it (production 68; the reference stays literal, nothing is fetched), matching the scanner-side split; the errata E15 family is enforced (any reference, comment, PI, or whitespace inside an element declared EMPTY; direct or double-escaped character-reference whitespace as character data in element content, while entity-supplied whitespace stays legal); errata E2 reports duplicate tokens in enumerated and NOTATION declarations; errata E14's declaration-completed-inside-a-PE-replacement parses and reports VC: Proper Declaration/PE Nesting; the ID family must be NCNames under namespace validation; and DTD-strict mode requires namespace declarations to be declared like any attribute. Remaining deviations are exact and classified: 24+1 grapheme-cluster lexing cases (a combining mark after an ASCII delimiter merges into one Swift Character), 18 namespace-constraint cases, 4 encoding-declaration mismatches, and E18 (per-entity base-URI tracking), each filed as its own issue.
-
W3C xmlconf japanese section, fully clean (#121, #126): all 13 cases (the same prose and parameter-entity DTDs encoded as UTF-8, UTF-16 in both byte orders, Shift_JIS, EUC-JP, and ISO-2022-JP) decode through PureXML's own byte decoder, parse, and DTD-validate cleanly with zero deviations; the optional encodings the suite classifies as TYPE='error' are all supported, so they are held to the must-parse bar.
-
W3C xmlconf IBM section, clean to two documented classes (#121, #125): the deepest block, 1094 cases. Valid 220/220 (parse + strict DTD validation), invalid 64/64 reporting, not-wf 529/810 rejected with the 281 accepted cases being exactly two documented classes: 279 cases from productions 85-89, the 1998 character-class appendices deleted by XML 1.0 Fifth Edition (whose count is asserted exactly so drift in either direction is caught), and 2 parameter-entity-in-internal-subset feature cases (the xmltest 160-162 class, headed for #128's strict profile). Closing the section's real gaps: a document without a root element is rejected (production 1); an entity reference in an entity literal must be a lexical Name or character reference (production 68); WFC: No Recursion is enforced at declaration over the combined general/parameter reference graph (CDATA-shielded), so a recursive pair is rejected without being referenced; an undeclared parameter-entity reference is a well-formedness error when the document is standalone or has neither an external subset nor parameter entities, and a deferred validity finding otherwise (the production-68 WFC/VC split, also applied to entity references in attribute defaults); a declared external PE the resolver refused is neither; and a content-model group opening in one PE replacement and closing in another is reported (VC: Proper Group/PE Nesting).
-
Standalone validity constraints (#134): a document declaring
standalone='yes'is now checked against its external-subset dependencies (2.9). The DTD scanner records declaration provenance (which entities, element models, and attribute lists came from the internal subset); the validator reports an externally-declared attribute default that would be supplied, an attribute value an externally-declared non-CDATA type would normalize, and whitespace inside externally-declared element content. The parser enforces the matching well-formedness side: a standalone document referencing an entity declared outside the internal subset reports it as undeclared (WFC: Entity Declared). This empties the entire Sun baseline: the section is fully clean at not-wf 56/56, valid 28/28, invalid 74/74, zero deviations, closing #124. -
DTD validity constraints, second Sun burn-down (#121, #124): the validator now enforces VC Root Element Type (the root element must match the DOCTYPE name, recorded by the scanner), VC Unique Element Type Declaration (duplicate
<!ELEMENT>reported, first stays in effect), VC No Duplicate Types in mixed content, VC One ID per Element Type, VC Notation Attributes (listed names must be declared) and VC Notation Declared (an NDATA entity's notation must exist), VC Attribute Default Legal across the tokenized types plus VC ID Attribute Default (an ID attribute must be #IMPLIED or #REQUIRED), lexical Name checks for ID/IDREF/IDREFS values, and CDATA sections as character data in element content even when empty. Strict mode additionally requires every attribute to be declared (VC Attribute Value Type; namespace declarations exempt). Declaration-level findings are computed once when the schema is built and reported at the document root. Sun invalid coverage moves from 43/74 to 62/74 reporting; the twelve remaining silent cases are one family, the standalone='yes' VCs, which need declaration-provenance tracking. -
Entity content splicing (#133): a general entity whose replacement text contains markup, directly or through entities it references, is now reparsed as content at the point of reference (4.4.2 Included), so
<p>text</p>inside an entity becomes apelement in the tree and in the pull-event stream, in both strict and recovering modes, instead of being included as escaped character data. The replacement is validated (the reference-time balanced-content WFC), checked for reference cycles, and budgeted against the amplification cap before being spliced into the reader's stream. Character references in internal entity literals now expand when the declaration is parsed for general entities too (4.4.5, previously parameter entities only), so<foo>stores real markup that splices, and the Appendix D double escape&#38;stores&and renders as a literal ampersand; the reference-time WFC accordingly validates remaining references instead of re-expanding them. Text-only entities keep the single-pass expansion path and its event coalescing. Closes the last two Sun valid deviations: the section is now 28/28 valid. -
Validity-layer conformance, first Sun burn-down (#121, #124): tokenized attribute values are normalized per 3.3.3 before validity checks (whitespace runs collapse, outer whitespace strips, CDATA untouched), so
noncesatisfies an enumeration and an entity-expanded IDREF resolves; an external parsed entity's leading text declaration is stripped per 4.3.1 (and validated: version optional, encoding required, standalone forbidden) instead of being included in the replacement text; character references in parameter-entity literals are expanded at declaration time per 4.4.5, making the spec's Appendix D%zz;example parse; and a PI target inside the DTD subset must be separated from its data (production 16). Sun baselines tighten to not-wf 55/56, valid 26/28, invalid 43 reporting; the two remaining valid failures share one cause, filed separately: general-entity replacement text containing markup is included as character data rather than reparsed into elements. -
W3C xmlconf Sun section runner (#121, #124): the first validity-layer runner. Sun's manifests classify cases three ways and the runner checks all three: not-wf rejected by the strict parser (54/56), valid documents parse AND DTD-validate cleanly (19/28), invalid documents parse but must report at least one validity error (43/74). The exact deviation baselines are carried in the test with their causes: tokenized attribute-value normalization (3.3.3), external-entity text declarations (4.3.1), declaration-time character-reference expansion in PE literals (4.4.5), the standalone VCs, ID/IDREF corners, #REQUIRED enforcement, per-type attribute VCs, and the root/DOCTYPE name match, the work list this issue burns down.
-
W3C xmlconf OASIS/NIST section, fully clean (#121, #123): a second opt-in runner (XMLCONF_ROOT-gated, suite never vendored) drives the oasis section from its own manifest, parsed with PureXML itself; all 247 not-wf cases are rejected and all 100 well-formed (valid + invalid) cases parse, with zero deviations (the one excluded case is flagged NAMESPACE='no' by the manifest itself: pre-namespace colons in names, which a namespace-aware parser correctly refuses). Closing the section's last 24 gaps made the DTD subset grammar strict: unknown, lowercase, or space-separated markup-declaration keywords are rejected; a parameter-entity reference must be '%' Name ';' exactly; entity, parameter-entity, and notation names must start with a NameStartChar; NDATA requires whitespace and a strict name; a notation declaration requires its identifier and a clean tail; entity value literals admit no bare '%' (production 9); conditional-section keywords are case-sensitive INCLUDE/IGNORE and sections must balance to their ']]>'; junk between declarations is not well-formed; a grammar violation in the external subset now rejects the document (previously swallowed); the external subset may open with a text declaration (version optional, encoding required, standalone forbidden, production 77); and a processing-instruction target must be separated from its data by whitespace (production 16). The new strictness also tightened the xmltest baseline from 180/186 to 181/186: a CDATA section in the internal subset (case 107) is now correctly rejected by the conditional-keyword check, leaving five documented deviations.
Changed
- Schema-consistency checks now run through the validation framework (#117 follow-up): the derivation
finalcheck and Particle Valid (Restriction) are composableValidation<SchemaTypeFact, CompiledSchemaFacts>rules (finalRespected,restrictionsAreSubsets) applied per named type, so a schema with several problems reports them ALL at once.SchemaError.finalViolationand.invalidRestrictionare replaced by.inconsistent([String])carrying every finding with its type's coding path; the finding texts are unchanged.checkRedefine,checkAllGroups, andnotASchemastay fail-fast throws because they examine the raw schema source, the compilation analog of well-formedness. - Document-order sorts no longer recompute root paths per comparison (#113): node-set ordering (the evaluator's order-and-dedup, EXSLT's set functions, string-value coercion's first-node lookup) goes through decorate-sort-undecorate helpers (
sortedByDocumentOrder(),firstInDocumentOrder()) that compute each node's order key once, turning O(n log n x depth) sorts into O(n log n).set:leading/set:trailingalso reuse one precomputed pivot key. Behavior is unchanged. - XPath compilation moved out of the per-node loops (#112): XSLT template selection now resolves each match pattern once per transform through a per-run match cache (the pattern's node set is computed once over the source tree, then membership is an identity lookup), instead of recompiling and re-walking the tree for every (node, template) pair; and the XSD identity validator compiles each selector and field XPath once per run through a query cache instead of once per visited element. Schematron already stored compiled queries. Behavior is unchanged; a multi-template shape test guards the path.
- The HTML document builder's body is now constructed as a live mutable
TreeNodetree with a single open-elements stack (the HTML5 model), attaching nodes to their parent as they open rather than accumulating children on close (#83, internal). Behavior is unchanged; this is the foundation the adoption agency algorithm needs to reparent already-built subtrees. - DTD attribute validation is now decomposed into five named, independently composable
Validationrules (#101):DTD.requiredAttributes,fixedAttributeValues,enumeratedAttributeValues,tokenizedAttributeTypes, andnotationAttributes, replacing the singleattributeDeclarationsrule. Each is removable by identity and isolation-tested one constraint at a time, honoring the validation-framework idiom. Behavior is unchanged. (XSD content validation stays one recursive rule by a documented scope decision, since its constraints are interdependent through type resolution; Schematron already exposes its rules as composableValidation<Node, Void>values.)
Added
- Parser strictness, first W3C-suite pass (#105 Tier 2, #120): the strict parser now rejects
--inside comments (including the--->ending), a raw<in attribute values, the literal]]>sequence in character data, attributes not separated by whitespace, and characters outside the XMLCharproduction in content, five new locatedParseErrorcases. Driven by a new opt-in runner for the W3C XML Conformance Test Suite's xmltest section (XMLTS_ROOT-gated; the suite is never vendored because its license permits redistribution only as the unmodified archive): valid/sa 118/119, not-wf/sa rejection improved from 80 to 180 of 186 and valid/sa to 119/119 across five strictness passes (the second adds character-reference validation, lowercase-x-only hex references and the referenced code point must be a valid XML Char; the reserved 'xml' PI target, legal only as the declaration at the document's very first bytes; and XML Char validation in comments and PI data; the third validates content models against the XML 1.0 grammar at DTD scan time, rejecting unbalanced parentheses, #PCDATA misplacement and nesting, mixed connectors, whitespace-detached or doubled quantifiers, empty groups, and SGML leftovers; the fourth makes the DTD declaration grammar strict: whitespace and name rules, junk after a value, NDATA spacing and its general-entities-only rule, entity-value literals checked for XML Chars and complete references, SYSTEM/PUBLIC identifiers parsed strictly with PubidChar validation, validated against productions 52-60, the reserved xml PI target rejected inside the subset, strict UTF-8 decoding, the XML declaration's version/encoding value grammar, and required whitespace between pseudo-attributes; the fifth implements the replacement-text well-formedness constraint: when a general entity is referenced, its replacement (character references expanded as at declaration) must reparse as balanced content in isolation, so a bare or incomplete reference, a raw '<' in an embedded attribute value, tags spanning the entity boundary, and a reserved-target PI inside the replacement are rejected at the point of use, while an unreferenced entity may carry any value and only the first declaration of a name binds, both per spec; references in attribute-list defaults must name already-declared internal entities with a non-recursive chain; and a CDATA section inside replacement text now shields its content from reference expansion. The six remaining suite deviations are deliberate: internal-subset conditional/CDATA sections and parameter-entity references are supported as features, and one case expects pre-Fifth-Edition name-character classes), with the exact remaining baseline carried in the test and the two remaining leniency classes (internal-subset DTD grammar, entity replacement-text well-formedness) tracked as #120. - XSD Particle Valid (Restriction) (#117): a complex type derived by restriction is now checked at schema compile to accept a structural subset of its base, covering element name matching, element-vs-wildcard admission, wildcard narrowing, occurrence-range subsumption, and the group recursions (sequence/all order-preserving with emptiable skips, choice mapping, sequence-into-choice, sequence-into-all, RecurseAsIfGroup). A structurally unfaithful restriction (widened occurrence, new element name, reordered sequence, content added to an EMPTY or required-into-empty base, an element outside a base wildcard's namespace) throws
SchemaError.invalidRestrictionwith the type, base, and reason. The spec's effective-total-range arithmetic for MapAndSum is approximated by per-particle checks, a documented simplification. - Conformance corpus harness (#105, Tier 2): the start of conformance-suite checking, expressed through the validation framework (the OpenAPIKit idiom). A
PureXML.Validation.ConformanceCaseis aValidatablenamed expectation (actual vs spec-authoritative expected output), andConformance.matchesExpectedis aValidation<ConformanceCase, Void>rule that emits one locatedValidationErrorper diverging case, so a whole suite reports every failure at once with the case name as its coding path. Seeded with a Canonical XML 1.0 corpus (attribute ordering, empty-element expansion, comment removal, CDATA-to-escaped-text, processing instructions, namespace rendering) an XPath 1.0 core-function corpus, and an XSD datatype/facet corpus (length, range, pattern, enumeration, lexical space), a RELAX NG pattern corpus (text, optional, repetition, choice, ordered group, attribute, interleave, empty), an XSLT 1.0 transformation corpus (value-of, for-each, if, choose, sort, variable, format-number, apply-templates, call-template), an HTML5 tree-construction corpus (void elements, implied tag closing, case normalization, the adoption agency, entities), a Schematron corpus (assert, report, rule context), a C14N exclusive/2.0 corpus (unused-namespace dropping, render-at-use, whitespace trimming), an XPath axes/predicates corpus (descendant, ancestor, sibling axes, positional and attribute predicates, union), a DTD content-model corpus (sequence, choice, occurrence indicators, mixed, EMPTY, ANY), an XInclude/XPointer corpus (substitution, text inclusion with escaping, fallback, relative href resolution; shorthand ids, element() navigation, xpointer() including the per-step predicate semantic), an XSD regex corpus (implicit whole-string anchoring, the character-class escapes, Unicode categories, quantifier ranges, alternation, class negation and subtraction), and an XML Catalog corpus (system/public/uri entries, rewrite prefixes, unmatched identifiers). The HTML corpus surfaced its first divergence (the fragment parser does not reconstruct active formatting elements like the document parser, tracked as #109). Growing the corpora against the official W3C/OASIS suites is the ongoing Tier-2 work. - EXSLT extension functions (#105, Tier 3): the
common,math, andsetsEXSLT modules, dispatched by resolving a function's prefix to its namespace (so any prefix bound to an EXSLT namespace works).math:min,math:max,math:highest,math:lowest,math:abs,math:sqrt(with the EXSLT NaN rule for non-numeric nodes);set:distinct,set:difference,set:intersection,set:has-same-node,set:leading,set:trailing(by node identity and document order);exsl:object-type.math:powerand the other transcendentals are omitted because they need a C math library, which the pure-Swift, Foundation-free target excludes;exsl:node-setawaits result-tree-fragment support. - Canonical XML 1.1 (#105, Tier 3): a
Canonical.Options.canonical11preset and amergeInheritedBaseoption. When canonicalizing a document subset, thexml:basevalues of the apex's omitted ancestors are merged into the apex by RFC 3986 reference resolution (vs. 1.0's nearest-ancestor rule), the apex's own relativexml:baseis resolved against that chain, andxml:idis no longer inherited;xml:langandxml:spacestill inherit the nearest. A self-contained RFC 3986 resolver (scheme/authority/path parsing, path merging, dot-segment removal) backs the merge. - Single-byte legacy encodings (#97): the byte decoder gains a table-based single-byte path and the full set of single-byte legacy encodings, selected by the XML declaration's
encodingname for both the whole-buffer and streaming decoders: the complete ISO-8859 family (parts 2 through 16), Windows-1250/1251/1253/1254/1255/1256/1257/1258, and KOI8-R/U. Most are vendored verbatim from the authoritativeunicode.orgmapping files; ISO-8859-5/9/15 are derived exactly; Windows-1254 composes from Windows-1252. Multi-byte CJK encodings (#99) are tracked separately.
Fixed
- XSD identity-constraint XPath compile errors are no longer swallowed (#111): a selector or field XPath that fails to compile is reported once up front as a located error ("identity constraint 'k': invalid field XPath '...'"), instead of
try?silently turning a schema author's typo into a disabled constraint. The broken query still evaluates as no-match afterwards, so the rest of the document is validated. - XSD type resolution no longer accepts invalid input silently (#110): an
xsi:typenaming an undeclared type is now a located error ("unknown xsi:type '...'") instead of a silent fallback to the declared type, in both the tree and streaming validators; andtypeReferencechains are resolved through one shared cycle-detecting resolver, so a circular chain reports "circular type reference '...'" instead of silently truncating after 64 hops (and no longer recurses without bound in the streaming shallow check, a latent crash). Completions use the same resolver, best-effort. - The HTML fragment parser (
HTML.parse) now reconstructs active formatting elements, matching the document parser and the HTML5 algorithm (#109, surfaced by the Tier-2 conformance corpus). When a formatting element is closed out of order it is re-opened for the following content, so<b><i></b>X</i>yields<b><i></i></b><i>X</i>(not<b><i></i></b>X), in both the fragment and document paths. - XSD identity-constraint errors now locate the offending field, not just the element (#104): a single-field
key/keyref/uniqueerror extends the coding path with the field itself, an attribute (... at path: list/item[2]/@id) or a child element (.../code), resolved from the constraint's field XPath. keyref scope resolution is confirmed to resolve a key declared on an ancestor element (cross-scope), with a test. Full XSD qualified-node-set conformance for pathological multi-level same-name nesting remains part of the conformance-corpus work. - XSD identity-constraint errors (
key,keyref,unique) now carry a coding path locating the actual offending element, instead of always rendering "at root of document". TheIdentityValidatorwalk threads a[PathKey]the same way the content-model validator does, and each error is located at the selected target node, so a duplicate-key failure reports "... at path: list/item[2]" and a dangling keyref "... at path: orders/line" rather than an empty path. This brings the identity-constraint rule into line with the validation framework's path-carrying contract, which every other rule already honored. The "no element declaration for the root" XSD error is likewise located at the root element, matching the namespace check beside it. - A
<template>element's flow content is now kept nested inside it (<template><div>x</div></template>) instead of being split out into the body (#83).templateis built as body flow content rather than routed to the head.
Added
- ISO-2022-JP decoding (#108, in progress): the byte decoder gains the WHATWG
iso-2022-jpalgorithm, selected by theiso-2022-jpdeclaration name. Unlike the other CJK encodings this one is stateful, escape sequences switch between ASCII, JIS X 0201 Roman (with the yen sign and overline), JIS X 0201 katakana, and the JIS X 0208 plane, which is resolved through the sharedjis0208table (no new data). The streaming decoder now honors the declared encoding from the XML declaration (the libxml2 behavior), not only a byte-order mark, so the single-byte and multi-byte CJK decoders, including the stateful ISO-2022-JP whose mode is carried across reads, work over an incremental byte source; a test asserts streaming agrees with whole-buffer decode for a single-byte, a CJK, and an ISO-2022-JP document. - EUC-TW decoding (#108): the byte decoder gains EUC-TW (CNS 11643), selected by the
euc-twdeclaration name, for both the whole-buffer and streaming decoders. ASCII passes through; a lead in0xA1-0xFEopens a two-byte plane-1 character;0x8Eopens a four-byte form whose second byte selects the plane. CNS planes 1 and 2, which carry essentially all real-world EUC-TW text, are vendored from the ICUeuc-tw-2014mapping (private-use fallbacks dropped); the rarely used planes 3-15 decode to the replacement character and are a documented follow-up. - Streaming (reader-driven) DTD validation (#107, in progress):
PureXML.validate(streaming:dtd:)andPureXML.Validation.StreamingDTDValidatorvalidate a document against a DTD while it is pulled event by event (the libxml2xmlTextReadervalidation model), holding only the open-element stack rather than the whole tree. Each element's content model and attributes are checked the moment it closes, by applying the exact per-element DTD rules to a shallow element synthesized from the streamed child names. Document-scoped ID/IDREF integrity is tracked in bounded sets (the identifier values, not the tree): a duplicate ID is reported at its repeat occurrence, and forward IDREFs resolve at the end of the stream. Streaming and tree validation report the same problems (a test asserts matching reasons), and errors carry a libxml2xmlGetNodePath-style coding path. RELAX NG streaming is also available viaRelaxNG.validate(streaming:): the Brzozowski derivative is itself incremental, so it is driven directly from the event stream, retaining only the residual pattern (theafter-threaded chain is the validation stack) and a light per-element frame for ignorable-whitespace handling; a differential test asserts it agrees with the tree engine across valid, invalid, whitespace, and mixed-content documents. XSD streaming is available viaSchema.Document.validate(streaming:): each element's type is resolved from its parent's content model (andxsi:type/typeReference) as it opens, and its attributes and content-model structure are checked shallowly as it closes (ComplexValidator.validateShallow), with a differential test asserting agreement with the tree validator. WildcardprocessContentsand substitution-group resolution in the streaming form stay on the tree validator. - Output encoding (#106):
Serializer.serialize(_:encoding:)andPureXML.serialize(_:encoding:)now emit bytes in a chosen encoding (the libxml2xmlSaveToFilename(..., encoding)model), not only UTF-8. A newPureXML.Parsing.ByteEncodercovers the Unicode transformation formats, the single-byte legacy families (ISO-8859, Windows code pages, KOI8), and the multi-byte CJK encodings (Shift-JIS, EUC-JP, EUC-KR, GBK, GB18030, Big5). The CJK and single-byte inverses are built by running the existing, tested decoders over every valid byte sequence and recording the shortest sequence per scalar, so each encoder is by construction the exact inverse of its decoder (no new data, no separately maintained encode tables); GB18030's astral range uses the four-byte pointer formula. The declaration carries the encoding's canonical name, a UTF-16/32 stream is preceded by a byte-order mark, and any scalar the target encoding cannot represent is written as a decimal numeric character reference, so output always round-trips. - Big5 (#103): the byte decoder gains the WHATWG
big5algorithm, for both the whole-buffer and streaming decoders, selected by thebig5declaration name (and thebig5-hkscs/cn-big5aliases). A lead in0x81-0xFEfollowed by a trail in0x40-0x7Eor0xA1-0xFEresolves through the index, covering Traditional Chinese and the HKSCS extensions including the astral CJK ideographs (Extension B and the Compatibility Supplement). Four pointers decode to a base letter plus a combining mark (a two-scalar sequence). The 18,590-entry index is vendored verbatim from the WHATWGindex-big5.txt. Big5 completes the major CJK families libxml2 reaches through iconv (Japanese, Korean, Simplified and Traditional Chinese). - Full GB18030 (#102): the byte decoder gains the GB18030 four-byte sequences on top of the GBK two-byte subset, for both the whole-buffer and streaming decoders, selected by the
gb18030declaration name. A lead in0x81-0xFEfollowed by a digit opens a four-byte form whose pointer is mapped through the WHATWGindex-gb18030-rangeslinear ranges, reaching the supplementary planes (the189000range maps toU+10000and beyond). The 207 ranges are vendored verbatim from the WHATWGindex-gb18030-ranges.txt. GB18030 is China's mandatory standard, supported in libxml2 through iconv. - CJK multi-byte encodings (#99, in progress): the byte decoder gains a multi-byte path and the WHATWG Shift-JIS and EUC-JP algorithms, for both the whole-buffer and streaming decoders, selected by the XML declaration's
encodingname. Shift-JIS covers ASCII, half-width katakana, and the JIS X 0208 plane; EUC-JP adds the0x8Ehalf-width katakana and0x8FJIS X 0212 leads. EUC-KR (CP949/UHC) decodes Hangul and Hanja through the WHATWGindex-euc-kr, and GBK (which also covers GB2312) decodes Chinese through the two-byte range ofindex-gb18030with0x80as the euro sign. The JIS X 0208/0212, CP949, and GBK indexes are all vendored verbatim from the corresponding WHATWGindex-*.txtfiles. This completes the CJK multi-byte encodings. - HTML named character references: the complete WHATWG named character reference set (#84), all 2,125 semicolon-form references including the astral mathematical alphanumerics and the 93 two-codepoint references, vendored verbatim from the authoritative
entities.json(replacing the previous 16-entry stopgap). Decoding stays longest-match with an optional trailing semicolon, on top of the existing numeric-reference validation. - HTML5 select content model (#83, stage 10): a
<select>now follows the in-select rules.<optgroup>closes an open<option>, a nested<select>closes the open one rather than nesting, and an<input>/<keygen>/<textarea>closes the select and is placed after it. In the "in select in table" case, a table-structural tag (caption/table/tbody/tfoot/thead/tr/td/th) closes a select that is inside a table before being processed. - HTML5 frameset documents (#83, stage 8): a
<frameset>after the head now produces a frameset document whoseframesetelement replaces the body (<html><head></head><frameset>...), with nested framesets and<noframes>handled, and<frame>is now a void element (it no longer wrongly nests). An ordinary document still produces a body. - HTML5 integration points (#83, stage 7): HTML content inside an SVG integration point (
foreignObject,desc,title) is now parsed in the HTML namespace rather than SVG, so<svg><foreignObject><div>puts thediv(and its descendants) back in HTML; re-entering<svg>inside an integration point switches back to the SVG namespace. - HTML5 table foster-parenting (#83, stage 6): stray flow content inside a table is now moved out rather than nested in the table structure, matching the HTML standard. A non-table element or character data appearing while a
table/tbody/thead/tfoot/tris the open node is inserted immediately before the table (<table><b>x</table>-><b>x</b><table></table>), while well-formed cell content is untouched. - HTML5 SVG attribute-name case adjustment (#83, stage 5): an SVG attribute written in mixed case is restored to its canonical camel case in the tree (
viewbox->viewBox,gradienttransform->gradientTransform, and the rest), applying only to elements in the SVG namespace. HTML attribute names are left as the tokenizer lowercased them. With this, SVG/MathML foreign content carries correct element and attribute names. - HTML5 adoption agency algorithm and active formatting elements (#83, stage 4): misnested formatting tags are now recovered exactly as the HTML standard prescribes. Overlapping tags nest (
<b><i></b></i>-><b><i></i></b>), content after an out-of-order close is re-wrapped by reconstructing the active formatting elements (<b><i></b>X</i>-><b><i></i></b><i>X</i>), and a block inside a formatting element reparents through the furthest-block path, including the canonical<p>1<b>2<i>3</b>4</i>5</p>-><p>1<b>2<i>3</i></b><i>4</i>5</p>. - HTML5 SVG element-name case adjustment (#83, stage 3): an SVG element written in mixed case (which the tokenizer lowercases) is restored to its canonical camel case in the tree, so
<foreignObject>,<linearGradient>,<clipPath>, and the rest carry their correct SVG names. Applies only in the SVG namespace; same-named HTML elements keep their lowercased name. - HTML5 foreign content (#83, stage 2): an
<svg>or<math>element and all of its descendants now carry the SVG (http://www.w3.org/2000/svg) or MathML (http://www.w3.org/1998/Math/MathML) namespace on theirQualifiedName, so foreign elements are distinguishable from same-named HTML; ordinary HTML content (including content after a closed<svg>) stays in no namespace. (SVG/MathML element- and attribute-name case adjustment, and HTML integration points, are a later refinement.) - HTML5 table tree construction (#83, stage 1): a
<table>written without its section and row wrappers now nests correctly. A<tr>directly inside a<table>gets an implied<tbody>, a<td>/<th>gets an implied<tr>(and section), consecutive rows share one implied<tbody>, and an explicit<thead>/<tbody>/<tfoot>is not duplicated, all inside an open table only. - HTML tokenizer correctness (#84, partial): numeric character references are now range/surrogate-validated (zero, surrogates, and values above U+10FFFF yield U+FFFD) with the Windows-1252 C1 fixup table (
€-> euro, etc.); named references are decoded by longest match with or without a trailing semicolon; a literal NUL byte becomes U+FFFD; RCDATA elements (title,textarea) decode character references while raw-text elements (script,style) take their content verbatim; and a single leading newline after<textarea>is stripped. (The full ~2,000-entry named-entity table remains deferred to vendoring from the authoritative WHATWG data, the same policy as the legacy encodings.) - HTML conformance validator in the composable
Validationidiom.PureXML.HTML.validationErrors(in:)(backed byPureXML.Validation.HTML) judges a parsed tree against the intrinsic HTML5 content-model invariants as named, removable, isolation-testableValidationrules, the same idiom as the DTD/XSD/structural validators:voidElementsAreEmpty(void elements carry no content),requiredParent(li/td/option/... appear inside an allowed parent, with the allowed parents named in the finding), anduniqueIdentifiers(idvalues are unique). Failures are locatedValidationErrors; this is the validation layer HTML previously lacked, so every parsing/schema subsystem now exposes its conformance checks as composable rules. - XInclude range inclusion (#87), completing the XInclude audit. An
xi:includewhosexpointerselects a range (xpointer(range(...)),range-to, orstring-range) now includes that range's content: when no node-selecting scheme matches, the processor falls back to the XPointer range model and substitutes each range's spanned nodes (or the matched text for astring-range). - XPointer
range()model (#88), completing the XPath/XPointer audit.PureXML.XPointer.evaluateRanges(_:over:)(andPointer.evaluateRanges(over:)) evaluate the range forms inside anxpointer()/xpath1()scheme:range(expr)(the covering range of each location),start/range-to(end)(one range spanning from the first location ofstartto the first ofend, the sibling run when they share a parent), andstring-range(location, "search"[, offset[, length]])(a character range per occurrence, with the XPointer offset/length adjustment). EachRangematerializes to the wholenodesit spans and thetextit covers, andxmlns()bindings apply to the range parts that follow, so this also unblocks XInclude range inclusion (#87). - C14N node-subset canonicalization and Canonical XML 2.0 parameters (#85), completing the canonicalization audit.
Canonicalizer.canonicalize(_ subtree: TreeNode)canonicalizes a fragment in place: the apex element renders the namespace context in scope from its omitted ancestors and inherits theirxml:base/xml:lang/xml:spacewhen it does not set them, so a signed fragment is independent of where it sat.canonicalize(_:including:)canonicalizes a node-set selected by aTreeNodepredicate (XPath/position-based selection): an excluded element's tags are omitted but its selected descendants are kept and re-declare their in-scope namespaces.Options.prefixRewrite = .sequentialrewrites every prefix to a canonicaln0/n1/... in document order (eliminating the default namespace and placing declarations at first use), andOptions.qnameAwareLabelsextends that rewrite into QName-valued attributes and elements, so documents differing only in prefix spelling canonicalize to identical bytes. - Serialization
textEscapinground-trip option (#91), completing the serialization audit.Options.textEscapingdefaults to.standard(escape only&,<,>);.roundTripalso escapes a carriage return as
in text and CDATA-as-text output, so a literal\rsurvives the parser's end-of-line normalization and the document round-trips byte for byte. (The CDATA-section vs escaped-text toggle,cdataAsText, was already present.) - DOM tree: first-class doctype, entity-reference, and namespace nodes, plus
ownerDocument,adopt, andimportNode(#90), completing the tree-model audit.TreeNodeKindgains.doctype,.entityReference, and.namespace, each with a factory (TreeNode.doctype(name:publicID:systemID:internalSubset:),.entityReference(_:children:),.namespace(prefix:uri:)) and typed accessors; doctype and namespace nodes drop out of the content value-Nodeprojection while an entity reference splices its replacement into place.ownerDocumentderives the owning document from the parent chain;adoptandimportNodemove or copy a subtree into another document, re-declaring every namespace it relied on from an outer scope so the result is self-contained. - Schematron
key(),document(), andcurrent()in tests (#80), completing the Schematron audit. Test, let, and message expressions can now resolve nodes throughxsl:keydeclarations, load an external document for cross-document checks (via an injecteddocumentLoader), and refer to the rule context node withcurrent(). - RELAX NG compact
>>follow-annotations (#79), completing the RNC syntax. A>>annotation after a pattern (a foreign element name with optional[ … ]content) is skipped, chaining and combining with leading[ … ]annotations; both carry no schema semantics. (Namespace declarations inside annotations are already dropped by the annotation skipper.) - XSLT
document()fragments and base-URI resolution (#82), completing the XSLT function/output audit. A#fragmentselects a subset of the loaded document via XPointer, a node-set first argument unions the documents named by each node, and a relative URI is resolved against a caller-suppliedbaseURI(also applied toxsl:include/xsl:import). - XSLT result-tree fragments usable as node-sets (#82). An
xsl:variablewith a body now binds a queryable document fragment instead of a plain string, socount($rtf/*),$rtf/child, andxsl:for-each select="$rtf/..."work; it still has its concatenated text as a string value. - XSLT
xsl:output cdata-section-elements(#82). The text content of the named result elements is emitted in<![CDATA[…]]>sections instead of escaped, leaving nested elements and other elements unaffected. - XSLT
xsl:outputdoctype-public/doctype-system(#82). When either is set, a<!DOCTYPE>for the result's root element (PUBLIC when both are given, SYSTEM otherwise) is emitted before the serialized output, for XML and HTML methods. - XSLT
key()over a node-set second argument (#82).key(name, node-set)(includingkey('k', .)) now unions the matches for every node's string value and de-duplicates, rather than using only the first node's string value. - XSLT
method="html"output (#82). Anxsl:output method="html"result is now serialized by the HTML rules (void elements like<br>emitted without a self-closing slash, raw-text elements like<script>/<style>left unescaped) instead of as XML. - XSLT
xsl:sort case-order(#82). Whencase-order="upper-first"/"lower-first"is set, the sort comparison is case-insensitive with the case used only to break ties among otherwise-equal strings; without it the default codepoint order is kept. - XSLT functions
generate-id,system-property,element-available,function-available, and anunparsed-entity-uristub (#82).generate-idreturns a stable per-node identifier (the context node when no argument is given, the empty string for an empty node-set);system-propertyreports the XSLT version (1.0) and vendor; the*-availablefunctions answer against the set of implemented instructions and functions. - XSLT
xsl:namespace-alias(#81), completing the XSLT top-level-element audit. A literal result element (and its attributes) in a stylesheet namespace bound bystylesheet-prefixis rewritten on output to the namespace and prefix bound byresult-prefix(#defaultsupported), so a stylesheet can emit literalxsl:elements. Folded acrossxsl:include/xsl:import. - XSLT
xsl:decimal-format(#81).format-numbernow honors named and default decimal formats: the decimal separator, grouping separator, percent, minus sign, zero-digit (so non-Latin digit sets render), NaN, and infinity symbols are taken from the chosenxsl:decimal-format, both in the picture and the output. Folded acrossxsl:include/xsl:import. - XSLT
xsl:fallback(#81). An unrecognized XSLT element instantiates itsxsl:fallbackchildren in its place (forwards-compatible processing), or is dropped when it has none; a fallback under a supported instruction is ignored. - XSLT
xsl:apply-imports(#81). Re-applies templates to the current node in the current mode, considering only templates of lower import precedence than the one being instantiated (so an overriding template can extend the imported one), falling back to the built-in rule when none match. - XSLT
xsl:attribute-set(#81). Named attribute sets are applied to a literal result element orxsl:elementviause-attribute-sets; sets may include other sets (recursively, with a cycle guard), set attributes are lowest precedence so the element's own attributes override them, and same-named attributes collapse to the last. Folded acrossxsl:include/xsl:import. - XSLT
xsl:message(#81). The message body is instantiated as diagnostic text;terminate="yes"aborts the transformation with that text as anXSLTError.terminated, and processing stops at that point. A non-terminating message produces no result-tree output. - XSLT
xsl:strip-space/xsl:preserve-space(#81). Whitespace-only text nodes are removed from the source tree for elements named bystrip-spacebefore transformation, withpreserve-spaceand a more specific name test winning over*, andxml:space="preserve"on a source element or ancestor overriding the stripping. Folded acrossxsl:include/xsl:import. - DTD located content-model errors. An element whose children break its content
model now names each stray child individually (
element <x> is not allowed in <r>), and reports a pure order/count violation once with the allowed elements as a hint (… do not match its content model; allowed: <a>, <b>), instead of a single opaque message, matching the editor-grade diagnostics of the other validators. - XSD located content-model errors. A content-model violation is now placed at
the offending child with a recovery hint naming what was expected there
(
element 'x' is not allowed here; expected <b>,content is incomplete; expected <b>), instead of one opaque "content does not match the content model" per element. Anxs:allgroup recovers past each stray child and reports every missing required member, and well-placed children are still validated for their own content, so an editor sees every problem at once. This brings XSD content validation to the same located, recovering standard as the RELAX NG, DTD, and Schematron validators. - RELAX NG located, recovering validation errors (#79). Alongside the boolean
validate(_:),RelaxNG.errors(in:)now reports every way a document fails the schema as individual located errors (each with a coding path), recovering past each failure so an editor can surface all of a faulty document's problems at once rather than only the first. Errors carry recovery hints (expected <a>, <b>) naming the element types accepted at the failure point, and distinguish unexpected elements, invalid or missing attributes, invalid text/datatype content, and missing required content. A datatype mismatch quotes the offending value and names the expected type ('abc' is not a valid integer), and a value mismatch quotes both the actual text and the required literal. An attribute failure distinguishes an undeclared attribute from a declared one with a bad value, quoting the value and naming its type.RelaxNG.validation()exposes the schema as aValidation<Node, Void>, so RELAX NG composes with the same validation framework as the XSD, DTD, and Schematron rules. The boolean engine remains the authority on validity; the walk only places and explains the failures. - XML catalog group-level
prefer(#86). Apreferattribute on agroup(or a nestedcatalog) now overrides the catalog-wide preference for thepublicentries inside it, inherited through nested groups. Resolution of an external identifier consults each public entry's own effective preference, so two entries in one catalog can prefer differently. A system match still always wins. - Schematron abstract patterns (#80). A
<pattern abstract="true">is a template whose rule queries carry$namereferences; a<pattern is-a="…">with<param name= value=>children instantiates it, substituting each parameter into the template'scontext,test,<let>value, and<value-of select=>in a single pass that never rescans a substituted value or lets a short name capture a longer one. The template itself contributes no rules, and$namereferences that are not parameters (ordinary<let>variables) are preserved. - RELAX NG conformance:
datatypeLibrary, value-space<value>, name-class subtraction, and compact-syntax breadth (#79). The in-scopedatatypeLibraryis now inherited from the nearest ancestor; the default library defines onlystringandtoken, the W3C XML Schema library defines the full built-in set, and a type no in-scope library defines is an unknown datatype, so its<data>or<value>matches nothing rather than validating silently. A<value>carries its datatype and compares in that type's value space, so1equals01and1.5equals1.50, booleans treat1/trueand0/falseas equal, andstring/tokencompare by their whitespace-normalized form. A newnsNameExceptname class backs<nsName>with<except>and the compactprefix:* - namesubtraction. The compact parser also skips[ … ]annotations, treatsdiv { … }as transparent grouping, and folds~string concatenation. - XSLT template parameters and modes (toward full parity, #61).
xsl:paramdeclarations (with defaults),xsl:with-paramonapply-templatesandcall-template, andmodeon templates andapply-templatesare now honored: a passed parameter overrides the declared default, and a mode routes nodes only to templates declared in that mode (the built-in rules recurse in the same mode). - Push / feed streaming API (#1), the last epic. A resumable scanner reports a
token only once its terminator is fully buffered and otherwise signals
need-more (the Expat
XML_TOK_PARTIALmodel), soPureXML.Parsing.PushParserparses input fed in arbitrary chunks withfeed(_:)/finish(), driving aSAXHandler, while retaining only the current incomplete token plus the open-element stack.PureXML.events(feeding:)exposes anAsyncThrowingStreamof events over any async sequence of text chunks. Splitting a document at any byte boundary, down to one character per feed, yields identical events. - HTML parser and serializer (#20, the libxml2
HTMLparser.h/HTMLtree.hmodel).PureXML.HTML.parsereads tag-soup HTML leniently (case-insensitive tags; quoted, unquoted, and boolean attributes; comments; doctype; character references) and builds a node tree handling void elements (no end tag), optional end tags (implicit close ofli,p, table rows and cells, and the like), raw-text elements (script,style), and unmatched end tags.PureXML.HTML.serializewrites it back with void elements unclosed and raw-text content unescaped. - XSLT 1.0 transformation engine, completing #3.
PureXML.XSLT.transformapplies a stylesheet to a source document by the XSLT processing model: it matches each node to the highest-priority template (or the built-in rules) and instantiates the template's sequence constructor. Implementsapply-templates(with template dispatch andsort),value-of,for-each,if,choose,element,attribute,copy,copy-of,call-template,variable, literal result elements, and attribute value templates, all over the existing XPath engine. The identity transform round-trips a document including its attributes. - XSLT 1.0 stylesheet model and parser (toward #3).
PureXML.XSLTgains a stylesheet model (template rules with match patterns and computed default priorities, the instruction sequence constructor, attribute value templates) and a parser that compiles anxsl:stylesheetdocument into it. Recognizes the XSLT vocabulary by namespace orxslprefix; other elements are literal result elements. Supportstemplate,apply-templates,value-of,for-each,if,choose,element,attribute,copy,copy-of,call-template,variable,text, andsort. - Schema conformance fixtures, completing XSD and RELAX NG validation (#2). A cross-cutting suite validates realistic schemas (a patterned, bounded, ID-keyed XSD purchase order and a RELAX NG contact card using interleave, attribute value choices, and optional/repeatable elements) against conforming and non-conforming instances, plus datatype boundary fixtures.
- RELAX NG validation by derivatives (toward #2).
PureXML.Schema.RelaxNGparses a RELAX NG schema in the XML syntax into a pattern grammar (element,attribute,text,empty,notAllowed,group,choice,interleave,optional,zeroOrMore,oneOrMore,mixed,list,ref/define,data,value, and the name classes), andvalidate(_:)checks an instance document by James Clark's derivative algorithm: the pattern is derived through each start-tag, attribute, text, and end-tag event, and the document is valid when the residual pattern is nullable. Handles interleave, recursive grammars, and reuses the XSD datatype library fordata/value. - XSD schema-document parser, completing the XSD validation pipeline (toward #2).
PureXML.Schema.Document(xsd)compiles a schema document into its global element declarations and named-type table, andvalidate(_:)checks an instance document end to end. The parser matches the schema vocabulary by local name and supports global and local element declarations, named and inline simple and complex types,sequence/choice/allmodel groups with occurrence, attribute uses, simple content (restriction/extension), the full facet set, and recursive types (via named type references resolved during validation). - XSD complex types and particles (toward #2 schema validation).
PureXML.Schema.ComplexValidatorvalidates an element against aComplexType: its attribute uses (required and typed, with unknown-attribute rejection), and its content model. Content isempty,simpleContent(text validated against a simple type),elementOnly, ormixed. Particles compile to a Thompson NFA over element names honoringminOccurs/maxOccurs, withsequence/choicecompositors;allgroups are validated order-independently by counting. Child elements are validated recursively against their declared types (the XSD Element-Declarations-Consistent rule makes the name-to-type map well defined). - XSD simple-type datatype library (toward #2 schema validation). The new
PureXML.Schemanamespace validates a lexical value against the XSD Part 2 built-in datatypes (string,boolean,decimal, the bounded integer family,float/double,duration, the eight date/time types,hexBinary,base64Binary,anyURI,QName, and theName/NCName/NMTOKEN/languagefamily) and the constraining facets (length/minLength/maxLength,patternvia the regex engine,enumeration,whiteSpace,min/maxInclusive/Exclusive,totalDigits,fractionDigits). Decimals are compared exactly with no floating-point loss, date/time fields are range-validated (leap years,24:00:00, timezone bounds), and the integer types enforce their intrinsic bounds. - Regular-expression engine (#30, the XML Schema regex flavor, gating XSD). The
new
PureXML.Regexnamespace compiles a pattern (literals,., character classes with ranges and negation, the escapes\d \D \w \W \s \S \i \I \c \C, single-character escapes, grouping, alternation, and the quantifiers?,*,+,{n},{n,},{n,m}) to a Thompson NFA and matches the whole string, asxs:patternfacets require. Matching is linear in the input with no backtracking blow-up.\p{...}Unicode category escapes are not yet supported. - Canonical XML (#26, the libxml2
c14n.hmodel). The newPureXML.Canonicalnamespace renders the C14N form of a node: namespace declarations sorted by prefix (default first) and attributes by namespace URI then local name, empty elements expanded to start/end pairs, CDATA folded into escaped text, and the C14N text/attribute escaping rules. Inclusive mode renders every in-scope namespace; exclusive mode renders only the namespaces an element and its attributes visibly use (with anInclusiveNamespacesprefix list). Comments are omitted unless requested. For whole-tree canonicalization C14N 1.0 and 1.1 coincide, so the single inclusive mode covers both. - XInclude, URI resolution, and
xml:base(#24, the libxml2xinclude.h/uri.hmodel). The newPureXML.XIncludenamespace adds RFC 3986 reference resolution (URIReference.resolve(_:against:), with dot-segment removal) andprocess(_:base:loadingURI:), which replacesxi:includeelements with their targets.hrefs resolve against the supplied base and the in-scopexml:base,parse="xml"includes the document element (or anxpointer()fragment viaPureXML.XPointer),parse="text"includes raw text, and a failed load usesxi:fallbackor errors. Includes nest. PureXML does no I/O itself: fetching is only through the injectedloadingURIclosure, so the default fetches nothing. - XML Catalog resolution (#27, the libxml2
catalog.hmodel). The newPureXML.Catalognamespace parses an OASIS XML catalog (matchingpublic,system,uri,rewriteSystem,rewriteURI, andgroup/catalogby local name) into aResolverthat maps public/system identifiers and URI names to replacement URIs, with longest-prefix rewriting and system-over-public precedence.Resolver.entityResolver(loadingURI:)builds aParsing.EntityResolverthat resolves an external identifier through the catalog and then loads the URI via an injected closure, so a catalog plus a loader wires external entities and DTDs in while the default (no loader) keeps XXE closed. - Schematron validation (#25, the libxml2
schematron.hmodel).PureXML.Validation.Schematron(schema:)compiles a Schematron schema (namespace-agnostic over the ISO and legacy namespaces) andvalidate(_:)checks a document: each<rule>'scontextXPath selects the nodes it fires on, and each<assert>/<report>testis evaluated relative to that node, a failed assert becoming an error issue and a matched report a warning. Within a pattern a node fires the first matching rule. Built on the XPath context-node evaluation, with a newXPath.Query.nodes(over:)that returns matched tree nodes for repeated querying of one tree. - XPointer (#23, the libxml2
xpointer.hschemes). The newPureXML.XPointernamespace resolves the shorthand bare-name form (name=id('name')), theelement()scheme (child-element positions from an id or the document root, e.g.element(intro/2),element(/1/2)), and thexpointer()scheme (a full XPath expression). A pointer may chain scheme parts; they are tried in order and the first non-empty selection wins (the fallback rule). Built onPureXML.XPath. - Streaming pattern matching (#22, the libxml2
pattern.hmodel). The newPureXML.Patternnamespace compiles the streamable XPath subset (element names,*,prefix:*,/,//, a leading absolute/, and a trailing attribute step) into aMatcher, rejecting predicates and./..as outside the subset.PureXML.Pattern.matches(_:in:)streams a document through the pull parser and returns the paths of matching nodes (/a/b/c,/a/@id) in document order, deciding each match from the open-element stack alone, so no tree is built.Matcher.matchesElement(path:)reuses a compiled pattern. - The XPath evaluation context, completing full XPath 1.0 (#21).
Query.value(at:position:size:variables:)evaluates an expression against an explicit context node already in a tree, with its proximity position, size, and variable bindings, soposition(),last(), and the upward axes resolve from that node. This is the per-node entry point downstream engines (XSLT, Schematron) drive. - The full XPath 1.0 function library (toward #21 full XPath 1.0). Beyond the
core functions, the string family (
concat,starts-with,contains,substring-before,substring-after,substring,string-length,normalize-space,translate), the number family (sum,floor,ceiling,round), and the node family (local-name,name,namespace-uri,id,lang). All pure Swift with no Foundation. Without a DTD,id()matches by an attribute namedidorxml:id. - The XPath expression engine and four-type model (toward #21 full XPath 1.0).
PureXML.XPath.Querynow compiles and evaluates full expressions, not just location paths: the operator grammar (or,and,=,!=,<,<=,>,>=,+,-,*,div,mod, unary-,|) with correct precedence, number and string literals, function calls, variable references, and filter expressions, all overPureXML.XPath.Value(node-set, boolean, number, string) with the spec's coercion rules and canonical number formatting. Predicates are now arbitrary expressions (a numeric predicate is a position test). Query gainsvalue(over:variables:),number,string, andbooleanaccessors, and a binding map for$variables. The core functionslast,position,count,not,true,false,boolean,number, andstringship with it. - All thirteen XPath axes (toward #21 full XPath 1.0).
PureXML.XPath.Querynow navigateschild,descendant,parent,ancestor,following-sibling,preceding-sibling,following,preceding,attribute,namespace,self,descendant-or-self, andancestor-or-self, with the./../@///abbreviations and theprocessing-instruction()node test. Evaluation runs over the parent-aware tree, so upward and sibling navigation are first-class; results come back de-duplicated and in document order regardless of axis direction. The attribute axis excludesxmlnsdeclarations and the namespace axis surfaces in-scope namespace nodes. - Pull-cursor reader (the libxml2
xmlTextReadermodel).PureXML.Parsing.TextReader(viaPureXML.reader(_:)) walks a document one node at a time withread(), exposingnodeKind,name,value,depth,attributes/attributeCount/attribute(_:), andisEmptyElementat each step. It layers on the streaming core, so it never holds the whole document. A childless element is reported once as an empty element (no separate end node), normalizing<a/>and<a></a>.documentTypesurfaces the DTD read so far as the validation hook. - Mutable, parent-aware document tree (the libxml2
tree.hmodel).PureXML.Model.TreeNodeis a reference type that knows its parent and siblings, so a document can be navigated upward and sideways (parent,nextSibling,previousSibling,ancestors,root,elementChildren,stringValue) and edited in place (append,insert(before:),insert(after:),removeFromParent,replace(with:),copy). Children are held strongly and the parent weakly; attaching a node detaches it from any previous parent, and a node can never become its own ancestor.parseTree(_:)builds one from XML andTreeNode.nodeconverts back to the value tree for serialization. - External and parameter entities (the libxml2
entities.hmodel), secure by default. Internal parameter entities (<!ENTITY % name "value">) are stored and expanded within the DTD, including bare%name;references that inject markup declarations, bounded by depth and the expansion budget. External general entities and the external DTD subset are recorded but never fetched by PureXML itself: they are loaded only through an injectedPureXML.Parsing.EntityResolver(a struct of closures). The default resolver refuses every external reference, so XXE stays closed; an external entity then fails as undefined and the external subset is left unread. Internal declarations win over the external subset. - Serialization option parity for the XML declaration (the libxml2 save-option
model).
PureXML.Emitting.OptionsgainsincludeXMLDeclaration,xmlVersion,encodingName, andstandalone; when requested, both the tree serializer and the incremental writer prepend an<?xml ...?>declaration from a single shared computation.Writer.writeStartDocument()emits it. Defaults stay off so fragment output is unchanged. - Spec-exact XML character classification (
PureXML.Parsing.XMLCharacter, the libxml2chvalid.hparity): the XML 1.0 (Fifth Edition)Char,NameStartChar, andNameCharproductions as public predicates over Unicode scalars, plusisValidName(_:). The scanner's name recognition now uses these exact ranges (replacing a looseisLettercheck) from a single source of truth, and handles both precomposed and combining-mark names. - Full encoding support. Byte input now decodes UTF-32 (both byte orders, BOM or
sniffed) and the single-byte encodings ISO-8859-1 and Windows-1252 selected by
the XML declaration's
encodingname, in addition to UTF-8 and UTF-16. The streaming byte path also decodes UTF-32. Standard library only, no Foundation. - Incremental XML writer (
PureXML.Emitting.Writer, the libxml2xmlTextWritermodel): emit a document with start/end and write calls without building a tree. Escaping matches the tree serializer (extracted into a sharedEscapinghelper), so compact output is byte-identical to serializing the equivalent tree. - SAX2 push-callback parsing (the libxml2 SAX2 model).
PureXML.Parsing.SAXHandleris a struct of optional callbacks (start/end document, start/end element, characters, CDATA, comment, processing instruction);PureXML.parse(_:sax:)drives them from the streaming core. Element names are namespace-resolved, so the URI is available on each callback. - DTD ID/IDREF validation. Attributes typed
IDmust be unique across the document;IDREFandIDREFSvalues must each resolve to a declaredID(forward references included, via a second pass). Completes the structural DTD validation layer alongside content models and attribute rules. - DTD attribute validation (
<!ATTLIST>). The parser surfaces attribute declarations alongside element models, andDTDSchemanow checks#REQUIREDattributes are present,#FIXEDattributes match when present, and enumerated attributes take a declared value. Tokenized types (ID/IDREF/NMTOKEN) are parsed but their value rules (uniqueness, references) are not yet enforced. - DTD content-model validation. The parser now surfaces the
<!DOCTYPE>internal subset's<!ELEMENT>declarations (PureXML.Parsing.DocumentType, viaParser.parseWithDocumentType), andPureXML.Validation.DTDSchemavalidates a tree against them:EMPTY,ANY,(#PCDATA), mixed content, and element content models (sequence/choice with?/*/+, matched as a regular language over child element names).PureXML.validateAgainstInternalDTD(_:)parses and validates in one call.<!ATTLIST>validation is not yet covered. - XPath query support (a practical subset).
PureXML.XPath.Querycompiles a location path and evaluates it over a node;PureXML.xpath(_:over:)is the one-shot convenience. Supports the forward axes (child, descendant//, self., attribute@), the node tests (name,*,text(),node(),comment()), and the predicates[n],[@a],[@a='v'],[child],[child='v']. Returns aSelectionnode-set withelementandstringValueaccessors. Upward/sibling axes and the full expression language are out of scope (the model has no parent pointers). - Optional DTD with internal general entities, off by default. With
Limits(allowDoctype: true)the parser reads a<!DOCTYPE>internal subset, honors internal<!ENTITY name "value">declarations, and expands them in text and attribute values. Defenses are mandatory: expansion is bounded byLimits.maxEntityExpansion(billion-laughs amplification raisesParseError.amplificationLimitExceeded), recursive entities raiseParseError.recursiveEntity, undeclared entities raiseParseError.undefinedEntity, and external entities are never loaded (XXE stays closed). The default still rejects<!DOCTYPE>outright. - Streaming byte decode.
PureXML.parse(pullingBytes:)andevents(pullingBytes:)accept an incremental byte source (() -> UInt8?) and decode UTF-8 or UTF-16 on the fly, so the bytes are never fully buffered. Encoding is detected from the leading bytes; invalid sequences become the Unicode replacement character rather than failing the stream. - Namespace resolution. Qualified names now carry an optional resolved
namespaceURI, populated by the parser from in-scopexmlnsdeclarations at the start-element boundary (the libxml2 SAX2 model). A default namespace applies to unprefixed element names but not attributes, thexmlprefix is built in,xmlnsdeclarations are preserved as attributes for round-trip, and an unbound prefix raisesParseError.undefinedNamespacePrefix. Namespace-free documents are unchanged (URIs stay nil). - Byte input with encoding detection.
PureXML.parse(bytes:)andPureXML.events(bytes:)accept raw[UInt8]and detect the encoding (UTF-8 or UTF-16, with or without a byte-order mark) following the XML sniff order, then decode with the Swift standard library (no Foundation).PureXML.Parsing. InputEncoding.detect(_:)exposes the detector. Malformed input (such as an odd-length UTF-16 stream) raisesParseError.malformedEncoding. - Configurable, bounded-by-default parser limits (
PureXML.Parsing.Limits): maximum nesting depth (default 256), name length, and content length, enforced during scanning. Protects against pathological input and keeps the recursive node model safe to build and tear down. Threaded throughparse/events. - Initial package scaffold: root Swift package, namespacing under
PureXML, and the portability and verification gates carried over from PureYAML. - XML node model (
PureXML.Model):Node,Element,Attribute, andQualifiedName, preserving document order and node kinds. - XML emitter (
PureXML.Emitting.Serializer) with pretty-printed and compact output and correct text and attribute escaping. - Structural validation (
PureXML.Validation.Validator) for duplicate attribute names. - Streaming, iterative XML parser.
PureXML.Parsing.EventReaderis a pull-based event core that consumes input through a character-source closure and emits oneEventat a time, holding only bounded state (an open-element stack and a small lookahead buffer). It never requires the whole document in memory and can drive chunked input.PureXML.Parsing.Parserbuilds aModel.Nodetree iteratively over the event core (no recursion). Handles elements, attributes, text, the five predefined entities and numeric character references, comments, CDATA, and processing instructions. Safe by default:<!DOCTYPE>is rejected (no DTD/XXE). - Top-level API:
PureXML.parse(_:),PureXML.parse(pulling:), andPureXML.events(_:)/PureXML.events(pulling:)for streaming.
Changed
- The emitter is now iterative (explicit work stack, not recursion), mirroring libxml2's approach, so deep trees do not overflow the call stack.
- The emitter escapes tab/newline/CR (and
>) in attribute values so attributes round-trip through spec-compliant parsers; mixed content is no longer reformatted (formatting is suppressed for elements with text/CDATA children).