A path that is a single forward-axis step from one context node returns its
nodes already in document order with no duplicates, so the document-order
sort is redundant. AxisNavigation produces the structural forward axes
(child, descendant, descendant-or-self, self, following, following-sibling)
in document order, deriving sibling order from the same children array the
order key compares, so the two can never disagree. This is the common shape
after //x compiles and fuses to a single descendant::x step.
The attribute axis (whose order interleaves with namespace declarations), the
namespace axis (produced in dictionary order), and every reverse axis
(nearest-first) are excluded and still sorted. Measured xpath ~7% faster on
the 20k-item generated corpus via a same-state A/B, identical results.
XPathSingleStepOrderTests pins document order for each included axis, that the
reverse axes still normalize, and that multi-step paths still sort. An
adversarial order-correctness review found no query whose order or uniqueness
the change breaks.
A path expression sorted its result into document order through orderUnique,
which first ran a Set-based de-duplication. But evaluateSteps already 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 that Set copied
every node for nothing, and each copy retains the wrapped tree node, whose
weak parent reference 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. Measured xpath ~9% faster on the 20k-item generated
corpus via a same-state A/B, identical results. The existing node-set
de-duplication and union tests cover that a path still yields each node once.
`//X` compiles to `descendant-or-self::node()/child::X`, which materializes
every node of the subtree as an intermediate context before selecting the X
children of each. Fuse that pair into a single `descendant::X` step at compile
time: the two select the same node-set, so evaluation skips the intermediate.
The fusion is transparent except for a positional predicate: `//x[1]` is the
first x child of EACH parent, not the first x in document order, so it must
not become `descendant::x[1]`. Apply the rewrite only when every predicate is
provably non-positional: its value is a boolean, node-set, or string (never a
number, which XPath reads as position() = value) and it nowhere references
position() or last(). Anything not provably both is left unfused, so a result
can never change.
Measured xpath ~10x to ~7x slower than libxml2 on the 20k-item generated
corpus via a same-state A/B, identical results. XPathDescendantFusionTests
pins the positional forms keeping their per-parent meaning, the non-positional
forms selecting across the subtree, and composition with trailing and nested
steps. An adversarial differential review (fused versus a spec-literal unfused
reference over nine attack suites) found no query whose result changes. The
boolean-function allowlist carries a documented invariant: every entry must be
position- and size-independent, since the position scan is syntactic.
Each location step accumulated its context nodes' axis results into a
Set<Node> to drop nodes reachable from more than one context, hashing and
copying every node. The wrapped TreeNode is reference-counted, so each copy
is ARC traffic, and the profile showed the Set insert, hashing, and Node
copies dominating evaluation.
De-duplication is needed only when two distinct context nodes can reach the
same node. Skip it on a single context and on the axes whose results are
disjoint per context (child, attribute, namespace, self): those produce the
identical node sequence the Set would, since every insertion succeeds, with
no hashing. Keep the Set on the axes that genuinely overlap (descendant,
ancestor, parent, the siblings, following/preceding). Predicates stay applied
per context and the path result still passes through the final
order-and-unique pass, so results are identical.
Measured xpath ~13x to ~10x slower than libxml2 on the 20k-item generated
corpus via a same-state A/B, ~28% faster, same result. XPathNodeSetDedupTests
pins that a node shared by many contexts through ancestor/parent/descendant
is still counted once, the disjoint axes return every distinct result, and an
overlapping union still de-duplicates. The full XPath suite and the Apache
Xalan XSLT gold-output corpus stay green.
The serializer's escaping functions rebuilt every text, attribute, comment,
and processing-instruction value character by character, even when the value
contained nothing to escape, which is the common case. Each now scans the
value's UTF-8 bytes for the bytes it would escape (the 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 returns the
value unchanged when none is present, taking the original character path only
when something must be escaped.
Measured serialize roughly halved, ~5.5x to ~2.7x slower than libxml2 on the
20k-item generated corpus, with byte-identical output. EscapingFastPathTests
pins both the unchanged fast path and the escaped slow path for each function,
and the Apache Xalan XSLT gold-output corpus (a serialize-heavy differential)
stays green. No output changed.
The attribute-value scanner ran the entity decoder on every value, and the
decoder re-scanned the whole value for '&' before doing nothing when none
was present. The byte-run scanner already visits every value byte, so it
now notes any '&' and an ampersand-free value (the common case) returns its
normalized run directly. Measured against a same-machine-state baseline,
every run came in below every baseline run, ~4% faster parse on the
20k-item generated corpus.
Move the bulk byte-run scanners (contentRunBytes, attributeRunBytes) into
ReaderByteRuns.swift and centralize their cursor mutation through a single
advanceByteRun method, so Reader's position fields stay write-encapsulated
in Reader.swift while both files stay under the length cap. The byte and
Character paths are held to identical decoded values by the differential
oracle, extended with ampersand-free, entity, and character-reference
attribute cases. No spec behavior changed.
cvc-elt.4.3 defers to cos-st-derived-OK, whose clause 2.2.4 makes a type
validly derived from a union when it is validly derived from one of the
union's member types. The block check followed only the named base chain,
so when the declared type was a union and the substitute reached it through
a member by a blocked derivation method, the check found no path and stayed
silent, wrongly accepting the substitution (W3C XSTS elemT074: block on the
element is restriction and the xsi:type restricts a union member).
Record a union's direct member types during schema compilation and follow a
derivation through a member when the base chain does not reach the union.
The member clause is existential: bar the substitution only when every
confirmed derivation path to a member is blocked, so a substitute that is
itself a member (its self-membership path uses no method) or reaches one
member by an unblocked method is accepted whatever other members it reaches
by a blocked method. The check adds a rejection only on a positively
confirmed path, so an unresolved, inline-anonymous, or nested-union member
leaves it silent: a bounded, disclosed under-rejection, never a false
positive.
Drops XSTS invalid-instances-accepted 15 to 14 with valid-schemas-rejected
and valid-instances-rejected held at 0 over the full archive. An adversarial
over-rejection review caught a first cut that order-shadowed the
self-membership path; the existential form rules it out and the case is a
regression test.
The element-content markup dispatch ran up to five literal string
comparisons (</, <!--, <![CDATA[, <?, <!DOCTYPE) before the start-tag
branch won, a per-element cost the parse profile indicted as
Reader.matchesBytes. Branch instead on the single byte after '<' on the
byte fast path: '/' end tag, '?' processing instruction, '!' a
declaration, otherwise a start tag.
The Character path keeps the equivalent matches()-based dispatch for a
streaming source or spliced entity text, and a '<!' that matches none of
comment/CDATA/DOCTYPE still falls through to the start-tag scanner with
the same expectedName error. Measured parse ~9.5x -> ~9.0x slower than
libxml2 on the 20k-item corpus; the byte and Character paths are held
identical by ParserPathDifferentialTests over its adversarial corpus,
and the Xalan, W3C, and RELAX NG suites stay green. No spec behavior
changed.
Keep the reader on its byte fast path through dispatch and name/attribute
scanning so plain-ASCII documents avoid the per-Character String machinery
(String.Iterator.next, string comparison, small-string materialization,
bridge-object ARC) that a profile indicted as the parse bottleneck.
The root cause was that a single buffered lookahead Character defeated every
existing byte path, each of which requires an empty buffer, and the content,
markup, and attribute dispatch each peeked a Character before scanning. Add a
non-buffering peekByte for dispatch, a byte-mode skipSpace, an all-ASCII
takeASCIIName, and an attributeRunBytes bulk scanner (with the 3.3.3
whitespace-to-space transform, deferring any carriage return to the Character
path so CRLF is not double-counted). Detect a text run's closing '<' via
peekByte instead of buffering it, which had cascaded the next element's whole
dispatch and name scan onto the slow path. Skip reference decoding for
ampersand-free text. The Character path remains the exact fallback for a
streaming source, spliced entity text, and any non-ASCII byte; the two paths
share scanContentMarkup so they cannot diverge.
Measured parse 20.1x -> 9.5x slower than libxml2 on the 20k-item generated
corpus, roughly a 53 percent wall-clock reduction. A differential oracle pins
the byte and Character paths to identical event streams and error marks over
an adversarial corpus, alongside boundary tests. Verified against the full
local suite, the Apache Xalan XSLT gold-output corpus, the W3C XML conformance
corpora, the RELAX NG spec suite, and the WASM build. No spec behavior changed.
* fix(xslt): keep the literal prefix for xsl:namespace-alias, remap only the URI
XSLT 1.0 7.1.1: an aliased result element keeps the prefix used in the
stylesheet; only its namespace URI is replaced by the result namespace. The
serializer adopted the result-prefix as the output prefix, so axsl:foo aliased
to the XSLT namespace came out as xsl:foo. The result-prefix now only selects
the replacement namespace; the element name, attributes, and copied namespace
declarations all keep their literal prefix.
Closes Apache Xalan conformance cases namespace19, namespace23, namespace24, namespace35, namespace113.
* refactor(xslt): drop the now-unused NamespaceAlias prefix field
The namespace-alias output keeps the literal prefix, so the result-prefix is no
longer stored; only the result namespace URI is needed. Removes the dead field
and corrects the addNamespaceAlias doc comment.
The xsl:key match was turned into a path by prepending //, but for a union
match like "Level1 | Level2" the // applied only to the first branch
(//Level1 | Level2), so the second branch matched relative to the root and
found nothing, dropping those nodes from the key. Distribute // over each
top-level union branch (// inside a predicate or function call is left alone).
Closes Apache Xalan conformance cases idkey45, idkey46, idkey47, idkey48.
XSLT 1.0 3.4: a whitespace-only text node in the stylesheet is preserved when
the nearest ancestor-or-self with an xml:space attribute has value preserve.
The body parser dropped every whitespace-only run unconditionally, so a literal
result element with xml:space="preserve" lost its whitespace content. Consult
the in-scope xml:space before stripping.
Closes Apache Xalan conformance case whitespace20.
XSLT 1.0 7.7: for level=single the count node may be the from-boundary node
itself when that node matches both the count and from patterns. The walk
stopped at the nearest from-matching ancestor before testing it against count,
so count="a|b" from="a" gave an empty number for a node inside an a. Test the
count pattern before the boundary stops the walk.
Closes Apache Xalan conformance case numbering63.
XSLT 1.0 7.7: the format attribute of xsl:number is an attribute value
template. It was stored and used as a literal string, so format="{$f}" was
never evaluated and a passed-in or computed format was ignored. Parse it as a
ValueTemplate and evaluate it per instantiation.
Closes Apache Xalan conformance case namedtemplate12.
XPath 1.0 4.2: an integer-valued number converts to a string with no decimal
point. The formatter only took the integer branch for a magnitude below 1e15,
so a value like 1234567890123456 (exact in a double, which holds integers
exactly to 2^53) printed as 1234567890123456.0. Raise the bound to 2^63, the
range where the Int64 conversion is safe; larger magnitudes still expand
through the exponent path.
Closes Apache Xalan conformance case string132.
The XPath grammar makes the relative part after / optional, but the compiler
only treated / as the root location path when it was at the end of the
expression (isAtEnd). When a / was followed by ), ], |, or an operator (as in
count(/), generate-id(/), or / = .), it fell through and tried to parse a step
from the terminator, yielding an empty node-set. Recognize / as the root
whenever no location step follows.
Closes Apache Xalan conformance cases position106 and mdocs15.
XSLT 1.0 5.5: the default priority of a single-step match pattern is set by its
node test, regardless of an explicit child:: or attribute:: axis. defaultPriority
matched the whole pattern string, so attribute::node() fell through to 0 (a name
test) instead of -0.5 (a node() node test), wrongly outranking @*. Strip a
leading child::/attribute:: axis before classifying the node test.
Closes Apache Xalan conformance cases conflictres29 and conflictres30.
XSLT 1.0 3.4: an xsl:strip-space/xsl:preserve-space elements name test is a
namespace-qualified NameTest, matched by namespace, not by prefix. A specifier
prefix:* or prefix:local now resolves its prefix to a URI at parse time, and a
source element is matched on its expanded name by specificity (exact {uri}local
over namespace wildcard {uri}* over *). The tests compared raw prefixed strings,
so ns1:* did not strip a source element bound to the same namespace under a
different prefix.
Closes Apache Xalan conformance cases whitespace06 and whitespace07.
XSLT 1.0 16.4: disabling output escaping for a value used as something other
than a text node (an attribute, comment, or processing-instruction string
value) is an error; the recovery is to ignore it. The attribute value built
from such content kept its raw-output markers, so the serializer emitted < and
> unescaped in the attribute. Strip the markers for these three node kinds, via
a shared helper, gated on the stylesheet actually using the feature so a
private-use character in ordinary data is never removed.
Closes Apache Xalan conformance case output75.
* fix(html): percent-escape non-ASCII characters in URI-valued attributes
XSLT 1.0 16.2 / HTML 4.01 B.2.1: the html output method percent-escapes
non-ASCII and control characters in attributes of type %URI (href, src, cite,
action, and the rest), writing each as the uppercase hex of its UTF-8 bytes,
while keeping " and & as entities. Other attributes are unaffected. The
serializer left such characters unescaped. Grounded in the libxml2
htmlSerializeUri study; reimplemented in pure Swift, no Foundation.
Closes Apache Xalan conformance case output32.
* docs(html): disclose URI-attribute escaping round-trip normalization
Address the URI-escaping critics: the html serializer follows libxml2's HTML
output model where URI-valued attributes are always percent-escaped, so a
parse-then-serialize round-trip normalizes a space or non-ASCII byte to %HH.
Document that this is idempotent (a literal % is left as is) and that every
space is escaped (leading ones included, where libxml2 skips them). Add an
idempotency test.
PI data may not contain ?>, which would terminate the instruction early. The
serializer emitted it raw, producing malformed output. Insert a space between
an offending ? and > (XSLT 1.0 7.3 recovery: a processor recovers from a
generated ?> by inserting a space after the ?), via a shared Escaping helper
applied at both XML PI serialization sites. A trailing ? is left alone.
Closes Apache Xalan conformance case output72.
* fix(xslt): serialize a namespaced element as XML under the html output method
XSLT 1.0 16.2: the html output method outputs an element whose expanded-name
has a non-null namespace URI as XML, not HTML. A namespaced document element's
tree was serialized as HTML, leaving empty namespaced elements unclosed (e.g.
a void name like br in a namespace) and repeating xmlns on every element, which
is not well-formed XML. Such a tree now serializes through the XML path (empty
elements self-close, the namespace is declared once and inherited); the html
method still omits the XML declaration. Null-namespace trees are unchanged.
Grounded in XSLT 1.0 16.2 and the libxml2 HTML serializer study; reimplemented
in Swift. Apache Xalan output63 exercises this rule but stays baselined because
its gold differs further only in non-normative attribute order.
* docs(xslt): disclose the document-element granularity of the 16.2 namespace rule
Make the whole-tree approximation explicit in the serializedBody comment and
the changelog: the html-outputs-namespaced-elements-as-XML rule is applied at
document-element granularity, not per element, so a mixed-namespace tree
follows its document element's path.
* fix(xslt): name the html output method doctype HTML, not the element name
XSLT 1.0 16.2: for the html output method the name following <!DOCTYPE should
be HTML or html, not the document element name (which is the xml method's rule,
16.1). The serializer used the document element name for both methods, so
method=html with a non-html root element (e.g. <root>) emitted
<!DOCTYPE root PUBLIC ...> instead of <!DOCTYPE HTML PUBLIC ...>.
Closes Apache Xalan conformance cases output40, output48, output60.
* fix(xslt): emit no html doctype when the result has no element
Addresses the doctype critic finding: a doctype precedes the first element
(16.2), so a text-only result must emit none even for the html method. The
fixed HTML name made the existing no-element guard unreachable; restore it by
guarding on the document element before choosing the name.
XSLT 1.0 16.2: the html output method escapes & and " in attribute values
but, unlike the xml method, leaves < (and >) literal. The serializer was
escaping < to < in attribute values. Removing that case lets it pass
through; the xml method is unchanged and still escapes both.
Closes Apache Xalan conformance cases output49 and output74.
* fix(xslt): resolve string-form document() against the stylesheet element's base URI
A string-form document() reference resolves against the base URI of the
stylesheet element holding it (XSLT 1.0 12.1). For a declaration in an
included or imported file that is the included file's URI, not the top
stylesheet's, so document('data.xml') (or document('')) in an included file
must resolve against the included file.
The parser already tracked each top-level declaration's source-file base but
discarded it after collection. That base is now carried onto the compiled
templates and global variables, into the runtime context, and through to the
function table, so the document() string form resolves against it. Templates
and globals both carry it, so the rule holds wherever the call appears.
Closes Apache Xalan conformance cases mdocs12 and mdocs13.
* fix(xslt): carry base URI into for-each and sort contexts; keep document('') self-referential
Follow-up to the string-form document() base URI work. Three coverage gaps:
- xsl:for-each and xsl:sort evaluate their body and key expressions in a
context derived from, not equal to, the enclosing template's, so a
document() call there resolved against the empty base when the template came
from an included file. Both now inherit the enclosing context's base URI.
- A non-empty base URI made document('') in the top stylesheet resolve to that
base and route through the loader, rather than returning the stylesheet's own
parsed document as before. The parsed stylesheet is now registered under its
base URI, so document('') reaches it without the loader; document('') in an
included file (a different base) still loads that file.
* fix(xslt): resolve document() references against the correct base URI
The document() function resolved every relative URI reference against the
single global base, so a reference relative to a document that document()
itself had loaded from a subdirectory could not be found. XSLT 1.0 12.1
defines the base per reference: the two-argument form against the second
argument's first node, the node-set form against each reference node's own
document.
Each loaded document now records the URI it came from as its base, and a
relative reference resolves against that base before the loader applies its
own source-document base. The common one-argument string form is unchanged
(its base is empty, deferring to the loader as before).
Closes Apache Xalan conformance cases reluri09, reluri10, reluri11.
* fix(xslt): record base URI for document() fragment selections
A fragment selection (document('x#frag')) is returned as a detached subtree
whose root identity differs from the cached whole-document tree, so its base
URI was not recorded. Using such a node as the base for a relative document()
reference then resolved against the empty base instead of the fragment's
document. Record the base on each fragment subtree's own root through a shared
helper, so baseURI(of:) finds it whichever node the reference is taken from.
XSLT 1.0 names every QName-keyed declaration by expanded name, so two prefixes
bound to the same namespace name the same thing. mode and variable names were
already resolved this way; named templates, xsl:key, xsl:attribute-set, and
xsl:decimal-format names were still keyed and compared as raw prefix strings.
A declaration name now resolves to {uri}local at parse time, and the references
resolve their prefix against the in-scope namespaces the same way: call-template
and use-attribute-sets at parse time, and the key() name argument and
format-number's format name at evaluation (a shared Library.expandedName helper
covers the two runtime sites). The parse-time expansion reuses a factored
expandedQName helper shared with mode and variable.
Closes Apache Xalan conformance cases namedtemplate16, attribset48, idkey53.
Full suite 1632 tests green in debug and release; the Xalan suite passes.
* fix(xslt): keep namespace nodes on an included literal result element
An xsl:include'd stylesheet's declarations are absorbed after collectUnit
returns, 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 lived only in the write-only retainedRoots array, so an
optimized build released them before the deferred resolution ran and the
in-scope xmlns declarations vanished, dropping the namespace nodes the
included element should carry. The output was correct in debug and wrong in
release. Pin the retained roots across the resolution with
withExtendedLifetime. Imports were unaffected because they compile eagerly
while their tree is still a live local.
Catches Apache Xalan conformance case lre/lre05, surfaced by running the
apache/xalan-test corpus.
* fix(xslt): select a called template by import precedence, not document order
xsl:call-template looked up the target with the first match by position in the
flattened template list. Imports fold in before the including unit's own and
included templates, so when an import and an include both define a template
with the same name, the imported one was wrongly chosen. An included template
carries the including unit's higher import precedence and must win (XSLT 1.0
section 6). The lookup now picks the highest-precedence definition among the
same-name templates.
Closes Apache Xalan conformance cases namedtemplate17, namedtemplate18,
namedtemplate19, and namespace13 (removed from the xalan-test baseline). Full
suite 1608 tests green in debug and release; the Xalan suite passes.
* fix(xpath): string-value of a comment or PI node is its own data
The XPath string-value of a comment node is its character data and of a
processing-instruction node is its data (XPath 1.0 5.6, 5.7), but TreeNode
returned the empty string for both, so value-of select="comment()", and any
matching, sorting, or comparison over comment/PI nodes, produced nothing.
stringValue was conflated with the text-only concatenation that forms an
element's or root's string-value, where comment and PI descendants correctly
contribute nothing (5.1, 5.5). That concatenation now uses a separate
textContent, so an element's string-value still excludes comment and PI text
while a comment or PI node reports its own data.
Closes 24 Apache Xalan conformance cases across node, axes, position, select,
modes, conflictres, and variable (removed from the baseline). Full suite 1609
tests green in debug and release; the Xalan suite passes.
* fix(xslt): resolve top-level variables in any order, not document order
Top-level variables and parameters may reference one another regardless of
declaration order (XSLT 1.0 section 11.4), but they were evaluated once
top-to-bottom, so a forward reference (a global 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 order settles in at most N passes, a pass that
changes nothing stops early, and a circular reference stops at the cap.
Extracted the evaluation into Transformer.evaluatedGlobals (XSLTGlobals.swift).
Closes Apache Xalan conformance cases variable33, variable34, variable35. Full
suite 1611 tests green in debug and release; the Xalan suite passes.
* fix(xslt): an empty-content variable with no select is the empty string
XSLT 1.0 section 11.2 makes <xsl:variable name="x"/> (empty content, no select)
equivalent to select="", so boolean() of it is false. It was always built as a
result-tree-fragment document node, which boolean() reports as true. A variable
with no select and empty content now yields the empty string; only non-empty
content remains a result tree fragment.
Closes Apache Xalan conformance cases boolean43 and boolean87. Full suite 1612
tests green in debug and release; the Xalan suite passes.
* fix(xpath): allow whitespace around the :: axis separator
XPath 1.0 allows whitespace between tokens, so `child :: sub` is the child axis
with node test `sub`. The parser only matched `::` immediately after the axis
name, so a spaced separator fell back to the child axis with the axis name read
as an element test, selecting nothing. parseAxis now skips whitespace after the
axis name and after `::`; on no match the reset to before the name is unchanged,
so a bare name step is unaffected.
Closes Apache Xalan conformance cases select16, select27, select28. Full suite
1613 tests green in debug and release; the Xalan suite passes.
* feat(xslt): document('') returns the stylesheet's own document
XSLT 1.0 section 12.1 makes an empty URI reference the document containing the
expression, so a stylesheet can read its own xsl:* nodes (embedded lookup
tables, self-inspection). The reference went straight to the document loader,
which has no entry for it, so document('') returned an empty node-set. The
transformer now keeps its parsed stylesheet tree and the document function
returns it for an empty reference, cached like any loaded document; a #fragment
still applies.
Closes Apache Xalan conformance cases select67, select68, idkey50, mdocs17,
namespace20. Full suite 1614 tests green in debug and release; Xalan passes.
* fix(xslt): implement unparsed-entity-uri from the source DTD
unparsed-entity-uri was a stub returning the empty string. It now reads the
source document type's unparsed (NDATA) entities, threaded to the function table
alongside the document loader, and returns the named entity's system URI; an
unknown name still returns empty.
Closes Apache Xalan conformance case expression02. Full suite 1615 tests green
in debug and release; the Xalan suite passes.
* fix(xpath): substring-after(s, '') returns s
Per the XPath 1.0 errata, an empty second argument matches at the start of the
string, so substring-after returns the whole string. The function bailed to the
empty string on an empty separator. substring-before(s, '') stays empty, as the
spec requires.
Closes Apache Xalan conformance case string143. Full suite 1615 tests green in
debug and release; the Xalan suite passes.
* fix(xslt): match a template mode by expanded name, not its 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. The prefixed QName string was stored and
compared verbatim, so apply-templates mode="foo:m" missed a template with
mode="moo:m" even when foo and moo share a namespace. Mode names on xsl:template
and xsl:apply-templates now resolve to {uri}local at parse time.
Moved the parser's declaration helpers to XSLTParserDeclarations.swift to keep
the file within the length budget.
Closes Apache Xalan conformance case modes16. Full suite 1616 tests green in
debug and release; the Xalan suite passes.
* fix(xslt): name a variable by expanded QName, not its 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. The declared name and the
$prefix:local reference were compared as raw strings, so $new:x did not see a
variable declared txt:x even when new and txt share a namespace. A prefixed
declared name now resolves to {uri}local at parse time, and the XPath evaluator
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 case variable55. Full suite 1617 tests green in
debug and release; the Xalan suite passes.
* fix(xslt): a match pattern predicate sees the global variables
XSLT 1.0 section 5.2 evaluates a pattern predicate with the top-level (global)
variables in scope, but pattern matching ran with no variables bound, so a
template like match="foo[. > $screen]" never matched ($screen was undefined).
The transformer supplies the global bindings to the match cache; they are
constant per transform, so they do not enter the cache key.
Closes Apache Xalan conformance cases match14 and idkey43. Full suite 1618 tests
green in debug and release; the Xalan suite passes.
* fix(emit): make a comment ending in a hyphen or containing -- well-formed
XML forbids `--` inside a comment and a `-` immediately before `-->`, so a
comment whose text ends with `-` was emitted as <!--text---> (malformed). The
serializer now inserts a space after each offending hyphen (the XSLT 1.0 16
recovery), so `text-` becomes `text- `. Applied in both the Serializer and the
streaming Writer via a shared Escaping.comment helper.
Closes Apache Xalan conformance cases output89 and output90. Full suite 1619
tests green in debug and release; the Xalan suite passes.
* fix(xslt): preserve whitespace adjacent to a CDATA section in a template
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 case whitespace13. Full suite 1620 tests green
in debug and release; the Xalan suite passes.
* fix(xslt): xsl:processing-instruction/comment keep only text-node content
XSLT 1.0 sections 7.4 and 7.6 make it an error for xsl:comment or
xsl:processing-instruction content to create a non-text node; the recovery is
to ignore the offending node together with its content. PureXML took the
string-value of all instantiated content, so a copied element leaked its text
into the PI or comment. The content now contributes only its text and CDATA
node items.
Closes Apache Xalan conformance case copy60. Full suite 1621 tests green in
debug and release; the Xalan suite passes.
* fix(xslt): xsl:attribute keeps only text-node content (errata E27)
XSLT 1.0 errata E27 makes it an error for xsl:attribute content to create a
non-text node, recovering by ignoring the offending node together with its
content (the same rule as xsl:comment/processing-instruction). PureXML took the
string-value of all instantiated content, so a copied element leaked its text
into the attribute value. The content now contributes only its text and CDATA
node items, reusing textNodesOnly.
Closes Apache Xalan conformance cases copy56 and copy58. Full suite 1622 tests
green in debug and release; the Xalan suite passes.
* fix(xslt): xsl:copy-of carries an element's inherited namespace nodes
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-of reproduced only the element's own xmlns attributes,
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 cases copy09, copy27, copy48, mdocs09. Full
suite 1623 tests green in debug and release; the Xalan suite passes.
* fix(xslt): xsl:element with an unusable name passes its content through
XSLT 1.0 7.1.2 makes a name that is not a QName an error; the recovery (the
pinned element-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. The name is now also rejected when
a part is not an NCName or its prefix is unbound.
Closes Apache Xalan conformance cases namespace40, namespace42, namespace43,
namespace60, namespace73, namespace79. Full suite 1624 tests green in debug and
release; the Xalan suite passes.
* fix(xslt): xsl:copy of the root node honors use-attribute-sets
Copying the root node produces no element of its own, so the use-attribute-sets
attributes have nowhere to attach on the copy and instead join the enclosing
result element (XSLT 1.0 7.5). copyInstruction dropped them for the root case;
they are now emitted ahead of the copied content so the enclosing element picks
them up.
Closes Apache Xalan conformance cases attribset27 and attribset29. Full suite
1625 tests green in debug and release; the Xalan suite passes.
* refactor(xslt): address review (canonical name/whitespace checks, html comment safety)
Review follow-ups on the conformance series:
- xsl:element name validation now uses the canonical Parsing.XMLCharacter
.isValidName instead of a hand-rolled grapheme check, which had narrowed valid
XML names (it dropped a name using a non-ASCII NameChar such as U+00B7).
- The template-body whitespace check reuses Parsing.XMLCharacter.isWhitespace
instead of a third local copy of the same predicate.
- The HTML serializer applies the section-16 comment recovery too, so a
method="html" comment ending in a hyphen (or containing two) is well-formed;
the C14N canonicalizer keeps comments verbatim as its contract requires.
Adds regression tests for a non-ASCII NameChar element name and for html
comment hyphen-safety. Full suite 1626 tests green in debug and release; the
Xalan suite passes.
A redefined model group whose body references itself expands, in the compiled
content model, to the original group it redefines. Two gaps let an ambiguity
created that way slip through the Unique Particle Attribution check (cos-nonambig).
The compositional determinism engine treated the self-reference as contributing
an empty decision set, so the original group's particles were invisible to the
overlap test. It now expands one level to the redefine base, exactly as the
instance content-model path does, so a particle the original contributes is seen.
The expansion only surfaces genuinely reachable items, so the decision sets stay
a subset of the true model: the check can catch a real ambiguity but never invent
one, holding the false-positive direction at zero.
Separately, the UPA check walked only the root document, so an ambiguity a root
redefine creates in 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 XSTS schN10 (a three-level redefine chain). XSTS invalid-schemas-accepted
11 -> 10, valid-schemas-rejected held at 0. Full suite 1609 tests green in debug
and release.
Generalise the wildcard {namespace constraint} model to the four XSD 1.0 (with
Errata E1-10) expressible forms -- any; a set (with absent); a pair of not and
a namespace name; a pair of not and absent -- replacing the any/##other/set
model that could not represent the union/intersection results. Both not forms
exclude absent, so a union admitting "every namespace except a name, plus
absent" (E1-10 rule 5.1.3) has no expressible form.
A new SchemaWildcardExpressibility compile rule rejects a complexContent
extension whose attribute-wildcard union with its base is not expressible
(src-ct.5), closing XSTS wildZ013 (test328873i). The expressible unions are
computed exactly in XSDComplexContent, so a derived type's wildcard admits the
correct attributes at instance time, closing wildZ013a and wildZ013d.
invalid-schemas-accepted 12 -> 11, invalid-instances-accepted 17 -> 15, with
valid-schemas-rejected and valid-instances-rejected held at 0 (full XSTS
archive). The model is additive for element wildcards (which never produce the
new forms); admits/narrows/UPA-overlap updated and verified unchanged for the
##other cases.
The intersection of two not forms negating different namespace names is not
expressible (src-ct.4), a deferred rule: rather than drop the wildcard and
over-reject valid cross-namespace attributes on an accepted schema, the
intersection stays the less-restrictive not(lhs) over-approximation (FP-safe,
matching prior behaviour). Union/intersection algebra taken clean-room from
Xerces-J XSWildcardDecl. Guarded by SchemaWildcardUnionExpressibilityTests.
#247 rejects an unescaped [ inside a regex character class, which closes the
W3C-settled invalid cases (RegexTest_993/1477) but also rejects siblings the MS
suite marks valid (([[:]+), ([[=]+), ([[.]+), RegexTest_989/990/991/1473-1475,
and the space-bearing subtraction [a - c - [ b ] ]+, 479/480). Those entries
are W3C-queried (status="queried", Bugzilla #4118) because the "valid" marking
contradicts the Appendix F grammar (SingleCharNoEsc excludes [); the suite's
settled position (993/1477) agrees with rejecting, so PureXML follows the
grammar. The queried siblings are excluded from the XSTS conformance gate, so
this is not a counted false positive.
This was shipped in #247 without surfacing the contested nature. Document the
reasoning at the error case and in the changelog so the spec-strict choice is
explicit, not silent. No behaviour change.
XSD charClassSub ::= (posCharGroup | negCharGroup) '-' charClassExpr is 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.matches applied the subtraction to the positive membership and
negated afterwards, so [^cde-[ag]] excluded only {c, d, e} and wrongly matched
a/g, accepting instance values a correct engine rejects. Negation now applies
to the base before subtraction (new result !B && !S is a strict subset of the
old !B || S, matching the Xerces-J reference exactly), so only negated classes
that also carry a subtraction change behaviour.
Closes XSTS RegexTest_430 and RegexTest_422: invalid-instances-accepted 19 ->
17, valid-instances-rejected and valid-schemas-rejected held at 0 (full XSTS
archive). Guarded by RegexCategoryTests.test_negatedSubtraction.
XSD 1.0 charProp is IsCategory | IsBlock, an exhaustive set: every Unicode
general category plus a fixed Unicode-3.1 block list. The block table 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, so parseCategory could not tell a real-but-untabulated block from a
bogus name and stayed lenient (RegexError.unsupported), accepting the pattern
\p{IsaA0-a9} and the schema declaring it (XSTS reK88).
Complete the table to the full XSD set: supplementary blocks with their ranges,
and surrogate blocks as valid names whose ranges hold no scalar (so they match
nothing). The recognised set is now authoritative, so parseCategory throws the
rejecting RegexError.invalidProperty for any unrecognised name. The now-dead
RegexError.unsupported case and the isMalformedProperty heuristic are removed.
Closes XSTS reK88: invalid-schemas-accepted 13 -> 12, valid-schemas-rejected
held at 0, instance buckets unchanged (full XSTS archive). Side effect: a
pattern naming a supplementary-plane block (e.g. \p{IsMusicalSymbols}) now
matches its characters instead of failing to compile, removing a latent
instance false positive. Block names/ranges taken from the XSD-supported list
in Xerces-J Token.java (clean-room: factual data only). The list is
spec-frozen at Unicode 3.1, so later blocks are rejected by design.
XSD 1.0 Datatypes Appendix F defines SingleCharNoEsc as [^\[\]] and admits a
nested class only as a -[...] subtraction, so a `[` inside a character class
that is not introduced by `-` is a syntax error. RegexParser treated such a
`[` as a literal scalar, so `[a[:xyz:]` compiled and the schema declaring it
was wrongly accepted (XSTS RegexTest_993, RegexTest_1477).
nextClassMember now throws a dedicated RegexError.unescapedClassBracket, kept
distinct from the generic, deliberately-lenient badClass so the change cannot
regress a valid-but-unsupported pattern. XSDSimpleParser.rejectsPattern treats
it as a genuine syntax error, so the declaring schema is invalid.
Closes RegexTest_993 and RegexTest_1477: invalid-schemas-accepted 15 -> 13,
valid-schemas-rejected held at 0, instance buckets unchanged (full XSTS
archive). Escaped [a\[c] and subtraction [a-z-[aeiou]] stay valid; the
unterminated case [a[:xyz: now faults on the inner [.
Compiling a schema with deeply nested, multiply-referenced model groups was
exponential: groupReferenceParticle inlined each <xs:group ref> into the Particle
content model, so g0=(g1,g1), g1=(g2,g2), ... built 2^K particles (depth 14 =
0.21s, depth 20 ~13-18s). The expansion is now memoized per (group definition,
cycle-visiting set) in a per-compile GroupParticleMemo shared across the value
copies XSDContext.scoped/visiting make, so sibling references share it and the
build drops to O(groups) (depth 40 now ~0.016s; previously infeasible).
The shared content term is reused by value (copy-on-write), so the compiled model
is identical and validation is unchanged: the XSTS gate holds (invalid-schemas-
accepted 15, valid-schemas-rejected 0, instance buckets unchanged) and the
streaming-vs-tree fuzz differential still passes. This closes the schema-compile
half of the interactive-safety group blowup (companion to the positionCap
determinism fix); a depth-40 compile test guards against regression. Full suite,
style, namespacing, and WASM all pass.
The Unique Particle Attribution check no longer inlines group references or caps.
A new CompositionalDeterminism engine computes first/followlast over particle
identities and summarizes each xs:group once, so a multiply-referenced nested
group whose inlined Glushkov automaton would be 2^K positions is checked in
O(nodes * particles^2) time. ContentModelDeterminism now delegates to it and the
inlining position automaton plus positionCap are deleted, closing the last named
interactive-safety resource-bound blocker (production-readiness stopper #4) and
the silent under-rejection the cap caused (stopper #3).
The compositional engine was proven verdict-equivalent to the prior inlining
automaton by a differential over the entire XSTS corpus (zero divergences) before
replacing it. The followlast detail was essential: a first attempt that emitted a
repeating end-item's loop-back and the suffix-first as separate decision sets
under-detected 13 XSTS content models; the differential caught every one.
Verification: XSTS gate unchanged (invalid-schemas-accepted 15, valid-schemas-
rejected 0, instance buckets held); SchemaDeterminismGroupBlowupTests compiles a
2^20-position model without hanging and rejects an ambiguous one the old cap would
have skipped (WASI uses a smaller depth, native time limits gated off WASI); full
suite, style, namespacing, and WASM all pass. The per-particle model and QName-only
overlap are preserved, so the false-positive direction stays at zero.
cos-ct-restricts 5.2: a complexContent restriction's content type is element-
only/mixed/empty (from its model group), which can never validly restrict a
simple content type. complexContentBaseKindFindings only flagged a simple-type
base; a complex type whose content is simpleContent slipped through. The check
is added for the restriction direction only (the extension direction has a valid
attribute-only idiom, handled by simpleContentExtensionBaseFindings). Catches
particlesZ039. XSTS invalid-schemas-accepted 16 to 15, valid-schemas-rejected
held at 0.
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/all that can contribute a child never validly restricts
a single base element. The oracle had a deliberate non-spec 'mirror' extension
that bounded the derived group by the element's effective occurrence range and
accepted it; that masked a real violation inside a choice-to-choice restriction,
where a derived sequence branch was allowed to map onto a base element branch it
cannot legally restrict. The (.group, .element) case now returns invalid; a
content-free derived group is still a valid restriction of any emptiable base
(handled before this case). Catches particlesHb011. XSTS invalid-schemas-
accepted 17 to 16, valid-schemas-rejected held at 0 (differential agree-valid
unchanged).
cvc-id: when an instance omits an attribute, its default/fixed value applies, so
a defaulted or fixed xs:IDREF/xs:IDREFS must still resolve to a matching xs: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
document-scoped ID/IDREF checking exactly as a present value would be (shared by
the tree and streaming validators, which both call validateAttributes). A
required absent attribute is still reported as missing, recordIDs is a no-op for
non-ID types, and a defaulted IDREF that does resolve stays valid. Catches
idZ012. XSTS invalid-instances-accepted 20 to 19, valid-instances-rejected held
at 0.
An xs:all group's own minOccurs="0" makes the GROUP optional (it occurs zero
or one times), not each member optional. The parser forced every member's
minOccurs to 0 whenever the all-group was optional, so a non-empty but
incomplete optional all-group (a required member missing) was wrongly accepted,
and a complexContent restriction that relaxed such a required member also slipped
past the particle-restriction check (the zeroed minOccurs made it look already
optional). Members now keep their declared occurrence; 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. Applied identically on the tree and
streaming paths (both call allStructureErrors). A required all-group is
unchanged. Catches mgZ001 (instance) and particlesK006 (schema). XSTS invalid-
instances-accepted 21 to 20 and invalid-schemas-accepted 18 to 17, both valid-
rejected buckets held at 0.
cvc-elt.3.2.2: an element nilled by xsi:nil="true" may not have a fixed
{value constraint} (a nilled element has no value, so it cannot take a fixed
one). A nillable element MAY declare a fixed value, so the schema stays valid;
the conflict is an instance error only when such an element is actually nilled.
The check rides the shared nilErrors chokepoint, so the tree and streaming
validators enforce it identically; it prefers the matched particle's constraint
and falls back to the by-local-name map for particle-less paths (the unique-
named root). A nillable element that supplies its fixed value, a nilled element
with no fixed constraint, and a nilled element with a default (not fixed) value
are all unaffected. Catches addB065. XSTS invalid-instances-accepted 22 to 21,
valid-instances-rejected held at 0.
The top-level declaration constraints (a global element/attribute may not carry
the local-only form/use/ref/minOccurs/maxOccurs; a global definition needs a
name) apply to every document in a schema composition, but the existing check
ran only over the document being compiled. A violation inside an xs:include,
xs:import, or xs:redefine target was therefore 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; redefine entries are skipped because each schema root walks its
own redefine children). The constraint is absolute and 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 to 18, valid-schemas-rejected held at 0.
SchemaStructure.swift was the last source file at the 400-line cap. The
cos-element-consistent check (a complex type's content model may not declare two
same-named elements with different types) is a self-contained constraint with its
own helpers (collectElementDecls, elementTypeKey) and the contentModelContainers
set used only by it.
Move that cluster into SchemaElementDeclarationsConsistent.swift;
elementDeclsConsistentErrors becomes internal (its sole caller is in
SchemaStructure). Pure code move, no behaviour change: SchemaStructure 400 -> 342,
new file 59. With this, no source file remains at the 400-line cap. Full suite and
the XSTS gate pass unchanged.
SchemaRedefineSelfReference.swift had reached the 400-line limit, blocking further
redefine-restriction work without a squeeze. It also mixed three cohesive concerns:
redefine self-reference well-formedness (src-redefine.6/7.2.1), attribute-group
restriction (cos-ct-restricts over the redefinition), and model-group restriction.
Move the attribute-group restriction machinery (redefineAttributeGroupRestrictionFindings
and its helpers) into SchemaRedefineAttributeGroupRestriction.swift, following the
established one-concern-per-file pattern (IdentityFieldType.swift). The shared base-
component lookups (baseComponents/resolveBaseComponent) become internal so both files
use them. Pure code move, no behaviour change: 171 + 232 lines (was 400); full suite
and the XSTS gate pass unchanged (20/0/22/0 buckets, 1558 unit tests).
cvc-identity-constraint.3 / c-fields-xpaths require an identity-constraint field to
identify a node with a simple type. The prior fix rejected a field whose selected
instance node carries element children (structural). This adds the case where the
field's DECLARED type is a complex type with non-simple content (empty or
element-only/mixed): 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 declared type, then the field's same-named child element in its content
model) and classified. 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. The resolution helpers live in a separate
extension file to keep the validator within the type-body limit.
This catches W3C XSTS idG006. XSTS invalid-instances-accepted 23 -> 22,
valid-instances-rejected held at 0.
The substitution-group filter drops a member when the head element's
{disallowed substitutions} (or the head type's {prohibited substitutions}) blocks
the method by which the member's type derives from the head's type. It looked the
member's type up in the named derivation backbone, so a member with an INLINE
(anonymous) complex type had no entry and was never dropped, even when its inline
type extended the head's type and the head blocked extension.
Record an inline-typed element's derivation under a synthetic key (the element key
plus a `#` marker no QName can contain, so it cannot collide with a real type and
is unreachable as any other type's derivation base) so the filter can see the
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.
This catches W3C XSTS disallowedSubst00105m (and a sibling). XSTS
invalid-instances-accepted 25 -> 23, valid-instances-rejected held at 0.
Per cvc-identity-constraint.3 / c-fields-xpaths an identity-constraint
(key/unique/keyref) field must evaluate to a node with a simple type: a
simple-typed element, an element with simple content, or an attribute. The
field-cardinality clause (at most one node) was enforced, but the simple-content
clause was not, so a field selecting a complex-content element was wrongly
accepted at instance-validation time.
Add a structural, false-positive-free check: 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. A simple-typed or simple-content
element (text only), an empty or text-only node, and an attribute are left alone,
so no valid field is rejected.
This catches W3C XSTS 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)
flattens a complex type's or attribute group's uses through attributeGroup
references. A reference to a redefined group whose redefinition self-references its
former self stopped at the cycle guard, so the original's attributes were never
reached and a duplicate they cause was missed.
Expand a redefinition's self-reference to the group's pre-redefine base definition
(already retained in the context for this purpose) so the original's attributes
join the set; a genuine non-redefine cycle still terminates without contributing.
Separately, exclude use="prohibited" attributes from the duplicate-name and
single-ID counts: a prohibited use is a removal marker, not a member of
{attribute uses} (XSD 3.4.2 / 3.6.2). This is the spec-correct reading and keeps a
self-reference that re-imports an attribute and then prohibits it from being
counted as a duplicate.
This catches W3C XSTS attQ011 and attQ017. XSTS schema invalid-accepted 22 -> 20,
valid-rejected held at 0.
A redefinition of an attributeGroup that self-references the original re-imports
all of its attribute uses, so directly re-declaring one of those attributes
produces two attribute uses of the same name, a duplicate forbidden by
ag-props-correct.2.
Flag a non-prohibited direct re-declaration of an attribute the original directly
declares, when the redefinition has a self-reference. To stay false-positive-free
the check fires only when both the original and the re-declaration land in no
namespace (a qualified attribute's target namespace depends on cross-document
form/chameleon resolution, so it is not compared); a self-referencing redefinition
that adds genuinely new attributes (attgC007/attgC038/groupB018) stays valid.
Also extracts the use-transition restriction errors into a helper to keep the
function within the complexity limit.
This catches W3C XSTS schM8. XSTS schema invalid-accepted 23 -> 22, valid-rejected
held at 0.
A redefinition of an attributeGroup is a restriction of the original
(src-redefine / cos-ct-restricts), so a re-declared attribute's type must derive
by restriction from the original's. The attribute-restriction checks covered
drop-required, re-introduce-prohibited, add-attribute, and relax-fixed, but not a
type change that widens rather than restricts.
Add a built-in-only, namespace-gated check: it fires only when both the original
and the re-declared attribute name a built-in type resolving to the XSD namespace
and the re-declared built-in does not derive from the original's in the XSD
derivation lattice (int does not derive from boolean). A user, inline, or absent
type on either side is not compared, so the check declines and stays lenient: an
absent original type is anySimpleType (which any type restricts), a user/inline
type's derivation is not resolvable here, and a user type whose local name
coincides with a built-in resolves to a different namespace and is not mistaken
for the built-in.
This catches W3C XSTS schM4 (a redefinition re-typing an attribute from boolean
to int). XSTS schema invalid-accepted 24 -> 23, valid-rejected held at 0.