Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 67 additions & 20 deletions src/main/java/org/rumbledb/compiler/InferTypeVisitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -178,6 +179,7 @@
import org.rumbledb.spark.SparkSessionManager;
import org.rumbledb.types.AttributeNodeItemType;
import org.rumbledb.types.BuiltinTypesCatalogue;
import org.rumbledb.types.DocumentNodeItemType;
import org.rumbledb.types.ElementNodeItemType;
import org.rumbledb.types.FieldDescriptor;
import org.rumbledb.types.FunctionSignature;
Expand Down Expand Up @@ -990,6 +992,10 @@ private boolean isFunctionArgumentCompatible(SequenceType actual, SequenceType e
// Runtime function conversion checks the type and size of the typed value, just as for casts.
return true;
}
if (TypeAtomization.containsNode(itemType)) {
// Function conversion atomizes the node, so its known typed value must match.
return isFunctionArgumentCompatible(TypeAtomization.inferType(actual), expected);
}
if (itemType.isSubtypeOf(BuiltinTypesCatalogue.untypedAtomicItem)) {
return actual.getCardinality().isSubtypeOf(expected.getCardinality());
}
Expand Down Expand Up @@ -1041,16 +1047,13 @@ public StaticContext visitCastExpression(CastExpression expression, StaticContex
return argument;
}

// The operand's arity counts source nodes, but casts constrain the atomic values
// produced by atomization. One node can yield zero values (a nilled element),
// one value, or multiple values (a schema list). The static node type here does
// not distinguish these cases, so the runtime checks the atomized cardinality.
boolean nodeOperand = TypeAtomization.containsNode(expressionType.getItemType());
if (!nodeOperand
&& !expressionType.isAritySubtypeOf(
SequenceType atomizedOperand = atomizedCastOperand(expressionType);
if (atomizedOperand != null
&& !atomizedOperand.isAritySubtypeOf(
expression.getSequenceType().getArity())) {
throwStaticTypeException(
"A cast expression operand must contain at most one item.", expression.getMetadata());
castCardinalityMessage(expressionType, atomizedOperand, expression.getSequenceType()),
expression.getMetadata());
}

// Check the type of result will casting to this schema type produce
Expand All @@ -1059,7 +1062,7 @@ public StaticContext visitCastExpression(CastExpression expression, StaticContex

if (resultType.getArity() == SequenceType.Arity.One
&& expression.getSequenceType().getArity() == SequenceType.Arity.OneOrZero
&& (nodeOperand || expressionType.getArity() != SequenceType.Arity.One)) {
&& (atomizedOperand == null || atomizedOperand.getArity() != SequenceType.Arity.One)) {
// Because getSimpleTypeCastResultType does not take into account the arity of the cast expression,
// this if-statement is needed to ensure that the result type is correctly set to OneOrZero when the
// cast expression has an optional arity.
Expand Down Expand Up @@ -1093,15 +1096,10 @@ public StaticContext visitCastExpression(CastExpression expression, StaticContex
}
}

boolean nodeOperand = TypeAtomization.containsNode(expressionSequenceType.getItemType());
// Cast cardinality applies after atomization. A source node can have zero, one,
// or multiple typed values, so its typed-value checks remain at runtime.
if (!nodeOperand && !expressionSequenceType.isAritySubtypeOf(castedSequenceType.getArity())) {
SequenceType atomizedOperand = atomizedCastOperand(expressionSequenceType);
if (atomizedOperand != null && !atomizedOperand.isAritySubtypeOf(castedSequenceType.getArity())) {
throwStaticTypeException(
"with static type feature it is not possible to cast a "
+ expressionSequenceType
+ " as "
+ castedSequenceType,
castCardinalityMessage(expressionSequenceType, atomizedOperand, castedSequenceType),
expression.getMetadata());
}

Expand All @@ -1123,6 +1121,22 @@ public StaticContext visitCastExpression(CastExpression expression, StaticContex
return argument;
}

/**
* Cast cardinality applies after atomization: one node can yield zero values (a nilled element), one value, or
* several (a schema list). Returns null when the typed value is only known at runtime.
*/
private static SequenceType atomizedCastOperand(SequenceType operand) {
if (!TypeAtomization.containsNode(operand.getItemType())) {
return operand;
}
return TypeAtomization.hasUnknownTypedValue(operand.getItemType()) ? null : TypeAtomization.inferType(operand);
}

private static String castCardinalityMessage(SequenceType operand, SequenceType atomized, SequenceType target) {
String found = operand.equals(atomized) ? operand.toString() : operand + " with typed value " + atomized;
return "with static type feature it is not possible to cast a " + found + " as " + target;
}

private boolean isSchemaCastTarget(SequenceType sequenceType, XmlSchemaCatalog schemaCatalog) {
ItemType itemType = sequenceType.getItemType();
return itemType.hasName() && schemaCatalog.isSchemaCastTarget(itemType.getName());
Expand All @@ -1147,9 +1161,10 @@ private void checkCastOperand(SequenceType operandType, Expression expression) {
}

private boolean isCastOperandTypeCompatible(ItemType source, ItemType target) {
// A node kind is not an atomic cast source; its typed value is checked at runtime.
return source.allMemberTypesMatch(
member -> TypeAtomization.hasUnknownTypedValue(member) || member.isStaticallyCastableAs(target));
// A node is cast through its typed value: an unknown one is checked at runtime, a known one statically.
return source.allMemberTypesMatch(member -> TypeAtomization.hasUnknownTypedValue(member)
|| TypeAtomization.atomizedItemType(member)
.allMemberTypesMatch(atomized -> atomized.isStaticallyCastableAs(target)));
}

@Override
Expand Down Expand Up @@ -3011,12 +3026,44 @@ public StaticContext visitValidateExpression(ValidateExpression expression, Stat
|| sourceItemType.isSubtypeOf(BuiltinTypesCatalogue.documentNode)
? sourceItemType
: BuiltinTypesCatalogue.nodeItem;
resultItemType = inferValidatedType(expression, sourceItemType).orElse(resultItemType);

// Successful validation always returns exactly one copied node.
expression.setStaticSequenceType(new SequenceType(resultItemType, SequenceType.Arity.One));
return argument;
}

/** Validating a document validates its single element child. */
private Optional<ItemType> inferValidatedType(ValidateExpression expression, ItemType source) {
if (source instanceof DocumentNodeItemType document) {
ItemType root = document.getElementTestType() == null
? BuiltinTypesCatalogue.elementNode
: document.getElementTestType();
return inferValidatedElementType(expression, root).map(ItemTypeFactory::documentNodeItemType);
}
return inferValidatedElementType(expression, source);
}

/** The validated copy of an element is annotated by its global declaration or by the requested type. */
private Optional<ItemType> inferValidatedElementType(ValidateExpression expression, ItemType source) {
if (!(source instanceof ElementNodeItemType element) || source instanceof SchemaElementNodeItemType) {
return Optional.empty();
}
XmlSchemaCatalog schemaCatalog =
expression.getStaticContext().getInScopeSchemaTypes().getXmlSchemaCatalog();
if (expression.getValidationMode() == ValidateExpression.ValidationMode.TYPE) {
return Optional.of(schemaCatalog.getElementTest(
element.getNodeName(), expression.getTypeName(), false, expression.getMetadata()));
}
// Strict validation requires a global declaration with the element's name (XQDY0084), so with a single
// declaration the name is known. Lax validation leaves an element without a declaration unannotated.
Name name = element.getNodeName();
if (name == null && expression.getValidationMode() == ValidateExpression.ValidationMode.STRICT) {
name = schemaCatalog.getOnlyElementDeclarationName().orElse(null);
}
return Optional.ofNullable(name).flatMap(schemaCatalog::findSchemaElementTest);
}

// endregion
//
// // begin scripting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,14 +281,12 @@ public static <EqNameCtx extends ParserRuleContext, StringLiteralCtx extends Par
: ItemTypeFactory.attributeNodeItemType(attributeName);
}
Name typeName = parseEqName.apply(attributeTestContext.typeNameEqName(), NameRole.TYPE);
return ItemTypeFactory.attributeNodeItemType(
attributeName,
typeName,
translationContext
.moduleContext()
.getInScopeSchemaTypes()
.getXmlSchemaCatalog()
.getTypeHierarchy(typeName, translationContext.metadata(attributeTestContext.context())));
return translationContext
.moduleContext()
.getInScopeSchemaTypes()
.getXmlSchemaCatalog()
.getAttributeTest(
attributeName, typeName, translationContext.metadata(attributeTestContext.context()));
}
if (kindTestContext.isCommentTest()) {
return BuiltinTypesCatalogue.commentNode;
Expand Down Expand Up @@ -353,14 +351,14 @@ private static <EqNameCtx extends ParserRuleContext> ElementNodeItemType element
: (ElementNodeItemType) ItemTypeFactory.elementNodeItemType(elementName);
}
Name typeName = parseEqName.apply(elementTestContext.typeNameEqName(), NameRole.TYPE);
return (ElementNodeItemType) ItemTypeFactory.elementNodeItemType(
elementName,
typeName,
translationContext
.moduleContext()
.getInScopeSchemaTypes()
.getXmlSchemaCatalog()
.getTypeHierarchy(typeName, translationContext.metadata(elementTestContext.context())),
elementTestContext.hasOptional());
return translationContext
.moduleContext()
.getInScopeSchemaTypes()
.getXmlSchemaCatalog()
.getElementTest(
elementName,
typeName,
elementTestContext.hasOptional(),
translationContext.metadata(elementTestContext.context()));
}
}
16 changes: 13 additions & 3 deletions src/main/java/org/rumbledb/types/AttributeNodeItemType.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ public class AttributeNodeItemType extends AbstractItemType {
@Getter
private List<Name> schemaTypeAlternatives = List.of();

/** The typed value, or null when it is only known to be xs:anyAtomicType*. */
@Getter
private SequenceType typedValueType;

public AttributeNodeItemType() {
this.catalogueName = Name.createVariableInDefaultTypeNamespace("attribute");
this.nodeName = null;
Expand All @@ -66,7 +70,8 @@ public AttributeNodeItemType(Name nodeName) {
this.schemaTypeHierarchy = List.of();
}

public AttributeNodeItemType(Name nodeName, Name schemaTypeName, List<Name> schemaTypeHierarchy) {
public AttributeNodeItemType(
Name nodeName, Name schemaTypeName, List<Name> schemaTypeHierarchy, SequenceType typedValueType) {
if (schemaTypeName == null || schemaTypeHierarchy == null || schemaTypeHierarchy.isEmpty()) {
throw new IllegalArgumentException("A typed attribute test requires a schema type hierarchy.");
}
Expand All @@ -75,11 +80,16 @@ public AttributeNodeItemType(Name nodeName, Name schemaTypeName, List<Name> sche
this.schemaTypeName = schemaTypeName;
this.schemaTypeHierarchy = List.copyOf(schemaTypeHierarchy);
this.schemaTypeAlternatives = List.of(schemaTypeName);
this.typedValueType = typedValueType;
}

public AttributeNodeItemType(
Name nodeName, Name schemaTypeName, List<Name> schemaTypeHierarchy, List<Name> schemaTypeAlternatives) {
this(nodeName, schemaTypeName, schemaTypeHierarchy);
Name nodeName,
Name schemaTypeName,
List<Name> schemaTypeHierarchy,
List<Name> schemaTypeAlternatives,
SequenceType typedValueType) {
this(nodeName, schemaTypeName, schemaTypeHierarchy, typedValueType);
this.schemaTypeAlternatives = List.copyOf(schemaTypeAlternatives);
}

Expand Down
2 changes: 2 additions & 0 deletions src/main/java/org/rumbledb/types/DocumentNodeItemType.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public class DocumentNodeItemType extends AbstractItemType {
private static final long serialVersionUID = 1L;

private Name catalogueName;
/** The test that the document's single element child matches, or null for document-node(). */
@Getter
private ItemType elementTestType;

public DocumentNodeItemType() {
Expand Down
27 changes: 24 additions & 3 deletions src/main/java/org/rumbledb/types/ElementNodeItemType.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ public class ElementNodeItemType extends AbstractItemType {
@Getter
private final boolean nillable;

/** The typed value of a non-nilled instance, or null when it is only known to be xs:anyAtomicType*. */
private SequenceType typedValueType;

public ElementNodeItemType() {
this.catalogueName = Name.createVariableInDefaultTypeNamespace("element");
this.nodeName = null;
Expand All @@ -71,7 +74,12 @@ public ElementNodeItemType(Name nodeName) {
this.nillable = false;
}

public ElementNodeItemType(Name nodeName, Name schemaTypeName, List<Name> schemaTypeHierarchy, boolean nillable) {
public ElementNodeItemType(
Name nodeName,
Name schemaTypeName,
List<Name> schemaTypeHierarchy,
boolean nillable,
SequenceType typedValueType) {
if (schemaTypeName == null || schemaTypeHierarchy == null || schemaTypeHierarchy.isEmpty()) {
throw new IllegalArgumentException("A typed element test requires a schema type hierarchy.");
}
Expand All @@ -81,15 +89,17 @@ public ElementNodeItemType(Name nodeName, Name schemaTypeName, List<Name> schema
this.schemaTypeHierarchy = List.copyOf(schemaTypeHierarchy);
this.schemaTypeAlternatives = List.of(schemaTypeName);
this.nillable = nillable;
this.typedValueType = typedValueType;
}

public ElementNodeItemType(
Name nodeName,
Name schemaTypeName,
List<Name> schemaTypeHierarchy,
boolean nillable,
List<Name> schemaTypeAlternatives) {
this(nodeName, schemaTypeName, schemaTypeHierarchy, nillable);
List<Name> schemaTypeAlternatives,
SequenceType typedValueType) {
this(nodeName, schemaTypeName, schemaTypeHierarchy, nillable, typedValueType);
this.schemaTypeAlternatives = List.copyOf(schemaTypeAlternatives);
}

Expand Down Expand Up @@ -126,6 +136,17 @@ public boolean isNodeItemType() {
return true;
}

@Override
public SequenceType getTypedValueType() {
if (this.typedValueType == null || !this.nillable) {
return this.typedValueType;
}
// A nilled element has an empty typed value.
return new SequenceType(
this.typedValueType.getItemType(),
this.typedValueType.getCardinality().union(SequenceCardinality.EMPTY));
}

@Override
public boolean isSubtypeOf(ItemType superType) {
if (superType.isUnionType()) {
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/org/rumbledb/types/ItemType.java
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ default FunctionSignature getSignature() {
throw new UnsupportedOperationException("getSignature operation is not supported for non-function item types");
}

/**
* The values that atomizing a node of this type produces, as declared by its schema type.
*
* @return the typed value type, or null when it is only known to be xs:anyAtomicType*
*/
default SequenceType getTypedValueType() {
return null;
}

// endregion

// region hierarchy
Expand Down
9 changes: 0 additions & 9 deletions src/main/java/org/rumbledb/types/ItemTypeFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -657,11 +657,6 @@ public static ItemType elementNodeItemType(Name nodeName) {
return new ElementNodeItemType(nodeName);
}

public static ItemType elementNodeItemType(
Name nodeName, Name schemaTypeName, List<Name> schemaTypeHierarchy, boolean nillable) {
return new ElementNodeItemType(nodeName, schemaTypeName, schemaTypeHierarchy, nillable);
}

/**
* Wildcard XQuery attribute node type attribute().
*
Expand All @@ -684,10 +679,6 @@ public static ItemType attributeNodeItemType(Name nodeName) {
return new AttributeNodeItemType(nodeName);
}

public static ItemType attributeNodeItemType(Name nodeName, Name schemaTypeName, List<Name> schemaTypeHierarchy) {
return new AttributeNodeItemType(nodeName, schemaTypeName, schemaTypeHierarchy);
}

/**
* Wildcard XQuery document node type document-node().
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ public boolean isSubtypeOf(ItemType superType) {
&& this.alternatives.stream().allMatch(type -> type.isSubtypeOf(superType)));
}

/** An instance may match any alternative, so its typed value is any of theirs. */
@Override
public SequenceType getTypedValueType() {
return this.alternatives.isEmpty() ? null : TypeAtomization.inferAlternativesType(this.alternatives);
}

@Override
public String toString() {
return "schema-element(" + this.declarationName + ")";
Expand Down
Loading
Loading