Validation

turbohtml validates XML against two schema languages – XSD 1.0 and RELAX NG – reusing the tree turbohtml.parse_xml() already builds. The whole engine lives in the C core; XMLSchema and RelaxNG are thin typed shims that compile a schema into a capsule once and hand each instance document to the extension. This page explains the two designs and why they differ.

Two engines, one instance tree

Both languages describe the same thing – which elements, attributes, and text an XML document may contain – but they express it differently, so the validator uses the algorithm each language is built for rather than forcing one shape on both.

XSD is a grammar of named declarations: global elements and types, referenced by name, with content models (sequence / choice / all and minOccurs / maxOccurs) and a rich datatype-and-facet layer on the leaves. The engine compiles a symbol table of the global declarations and then interprets the schema tree directly: it walks the instance element against its declaration, matching a content model with an NFA-style reachable-position set so repetition needs no backtracking, and resolving each leaf to a built-in datatype plus the facets gathered up its restriction chain. Namespaces resolve from the in-scope xmlns declarations, so targetNamespace and elementFormDefault="qualified" validate correctly.

RELAX NG is a pattern algebra – element, attribute, group, choice, interleave, oneOrMore, text, data, value, list, and ref – and it is validated by James Clark’s derivative algorithm (“An algorithm for RELAX NG validation”). The schema compiles to that algebra, and validation takes the derivative of the pattern with respect to each start tag, attribute, text run, and end tag: the pattern that remains after consuming one piece of the document. This is what makes interleave fall out for free – the derivative of interleave(p1, p2) over an element is the choice of advancing either side – with no backtracking and no combinatorial blow-up, because smart constructors absorb notAllowed and empty to keep the residual pattern small.

Why the C core

The datatype and facet layer is where validation spends its time: every leaf value is checked against a lexical space (is 2020-13-40 a date?) and then against its constraining facets (minInclusive, pattern, length, …). Doing that in the extension – over the code-point buffers the parser already produced, with a compact Thompson-NFA matcher for the pattern facet – keeps a schema check close to the cost of the parse it follows, rather than a second pass in Python. The recursion guards that protect the RELAX NG derivative from schemas whose refs recurse without an element in between live there too, so an adversarial schema fails cleanly instead of overflowing the stack.

What a result carries

validate() returns a ValidationResult: a valid flag and a tuple of ValidationError records. Each error carries a message, the document-order path that locates the node, a source line (0 when the tree carries no positions), and a coarse type"structure" for a content-model or attribute violation, "datatype" for a value outside its type’s lexical space, and "facet" for a value that fails a constraining facet. A RELAX NG failure is localized to the single element whose content or name the schema rejects, so the path points at the offending node rather than the document root.

See also

Validate against an XML schema for the task recipes and Validation for the full API.