Skip to content

Add initial support for schema types in static typing system - #1889

Draft
CaiJimmy wants to merge 8 commits into
jimmy/fix-element-typefrom
jimmy/schema-static-types
Draft

CaiJimmy wants to merge 8 commits into
jimmy/fix-element-typefrom
jimmy/schema-static-types

Conversation

@CaiJimmy

@CaiJimmy CaiJimmy commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Static typing now uses the schema types of nodes. Typed node tests (element(*, T), attribute(*, T), schema-element(E), schema-attribute(A)) know the typed value their schema type declares, and validate expressions infer the schema type they annotate their result with.

Based on #1881. It also contains the two commits of #1884 (schema type names keep their import prefix), which it needs; they will disappear from this diff once #1884 merges.

Typed values

Atomizing a node now yields the typed value its schema type declares, instead of always xs:anyAtomicType*. An instance may select a derived type with xsi:type, so the typed value covers the type and every type derived from it.

Examples from static-typing/schema-typed-values/ (types from TypedValues.xsd):

Expression Inferred type Why
validate { <t:price>4.5</t:price> } + 1 xs:decimal t:Price restricts xs:decimal
data(validate { <t:amount currency="CHF">3</t:amount> }) t:Price simple content with an attribute
data(validate { <t:optional-price .../> }) t:Price? nillable, and a nilled element has no typed value
data(validate { <t:tags>a b</t:tags> }) xs:string* list type
data(validate { <t:note>a<t:b>b</t:b></t:note> }) xs:untypedAtomic mixed content
data(validate { <t:marker/> }) xs:untypedAtomic? empty, but the derived t:Remark adds mixed content
data(validate { <t:order>...</t:order> }) xs:anyAtomicType* element-only content has no typed value

Item types report this through ItemType.getTypedValueType() (null when unknown), which TypeAtomization and the LSP can use.

How the typed value comes from the Xerces model

XmlSchemaCatalog already mapped a Xerces simple type to a Rumble atomic type (t:Price becomes a DerivedAtomicItemType based on xs:decimal), and any type to an annotation (its name and base-type chain). This PR adds a third mapping, from a type definition to the SequenceType of its typed value, or to unknown when it is no narrower than xs:anyAtomicType*.

Simple types:

  • An atomic type or pure union has exactly one value of its mapped type: t:Price gives t:Price.
  • A list, or a union with a list member, gives the list's item type with *: t:Tags gives xs:string*.
  • A union with constraining facets has no XDM type, so it uses the nearest base type that has one.
  • xs:anySimpleType stays unknown, since its values may also be lists.

Complex types: an instance may select a derived type with xsi:type, so the typed value covers the type and every complex type derived from it, joined with leastCommonSupertypeWith:

Content type Typed value
empty ()
mixed xs:untypedAtomic
simple the typed value of its simple type
element-only none: atomization raises FOTY0012, so it adds nothing

For example t:Marker is empty, but t:Remark extends it with mixed content, so it gives () ⊔ xs:untypedAtomic = xs:untypedAtomic?. If nothing but the empty sequence remains, the typed value is unknown.

Xerces only lists named types, so the catalog collects every complex type once when it is built, including the anonymous types of global and local element declarations, and finds derived types by scanning them with derivedFromType. Typed values are cached per type definition because of that scan.

Two rules live in the node types rather than the catalog:

  • ElementNodeItemType adds the empty sequence for a nillable element, since a nilled element has no typed value.
  • SchemaElementNodeItemType combines the typed values of its alternatives: the declaration and the members of its substitution group.

Known approximations:

  • Facets are not carried over, so t:Price is statically just a restriction of xs:decimal.
  • Derived types are joined by their least common supertype, not into a union, so content types that disagree widen to xs:anyAtomicType.

Validate expressions

Expression Inferred type
validate { <t:price>4.5</t:price> } schema-element(t:price)
validate type t:Price { <total>4.5</total> } element(total, t:Price)
validate lax { <t:unknown/> } element(t:unknown) (no declaration, so not annotated)
validate { document { <s:root>1</s:root> } } document-node(schema-element(s:root))

Strict validation requires a global declaration with the element's name (XQDY0084), so when the element's name is not known statically but the schema has a single global element declaration, that declaration is used.

Checks that use typed values

Operations that atomize their operands now check known typed values statically, and only leave nodes whose typed value is unknown to runtime:

  • Function arguments: sum(validate { <t:price>4.5</t:price> }) is xs:decimal, and sum(validate { <t:tags>a b</t:tags> }) is a static error because the typed value is xs:string*.
  • Casts check both the type and the cardinality of the typed value. Casting a list-typed element to a single type, or a nillable element to a non-optional type, is now a static XPTY0004; the message names the typed value, e.g. schema-element(t:tags) with typed value xs:string*.

TypeAtomization.containsNode no longer counts the error type (the type of fn:error()) as a node: it is also atomic and atomizes to itself, which made function argument checks recurse forever (K-FunctionProlog-66, xs-error-017, xs-error-019).

Name the prefix lookup, including why types in no namespace are skipped, and
check only the in-scope type name and a constructed value's type, for two
import prefixes.
@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

QT3 Test Results - Parser: jsoniq

Regression baseline: run 37837493959

🎉 No regressions found for this build.

Summary of passed tests for jsoniq
Test Suite Passing Failing Errors Skipped Missing features Total
app 2056 27 69 5 5 2157
array 208 0 1 0 0 209
fn 10311 268 169 387 315 11135
map 220 0 0 0 0 220
math 149 0 0 0 0 149
misc 765 34 20 52 18 871
op 4351 3 12 0 0 4366
prod 11505 328 166 208 7 12207
ser 332 11 0 0 0 343
xs 155 0 0 9 0 164
Total 30052 671 437 661 345 31821

Full analysis report: see analysis-jsoniq in artifacts.

Download detailed test results

@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

QT3 Test Results - Parser: xquery

Regression baseline: run 37837493959

🎉 No regressions found for this build.

Summary of passed tests for xquery
Test Suite Passing Failing Errors Skipped Missing features Total
app 2063 26 63 5 5 2157
array 209 0 0 0 0 209
fn 10333 247 168 387 315 11135
map 220 0 0 0 0 220
math 149 0 0 0 0 149
misc 769 30 20 52 18 871
op 4351 3 12 0 0 4366
prod 11588 279 132 208 7 12207
ser 343 0 0 0 0 343
xs 155 0 0 9 0 164
Total 30180 585 395 661 345 31821

Full analysis report: see analysis-xquery in artifacts.

Download detailed test results

fn:error() has the error type, which is both a node and an atomic type and
atomizes to itself, so the known typed value check recursed forever.
- Treat the error type as atomic in containsNode, so no caller atomizes it
  forever, instead of guarding one caller.
- Build typed element and attribute tests in the catalog, which looks up both
  the type hierarchy and the typed value, and drop the factory overloads.
- Share the fallback for xs: types that Xerces does not define, and look up an
  element declaration once with findSchemaElementTest.
- Cache typed values per type definition, since finding derived types scans
  every complex type, and stop at the second global element declaration.
- Assert that element-only content leaves the typed value unknown, and drop a
  runtime instance of check that schema-node-tests already covers.
Add ItemType.getTypedValueType, null when only xs:anyAtomicType* is known.
Element types now add the empty sequence for nillable elements themselves,
and schema element tests combine their alternatives, so TypeAtomization no
longer checks node kinds in a fixed order.
A cast takes one atomized value. Nodes skipped the cardinality check, even when
their typed value was known, so casting a list-typed or nillable schema element
to a single type only failed at runtime. Now only nodes whose typed value is
unknown are left to runtime, and the error message shows the typed value.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant