net.sf.saxon.expr.ForExpression Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of Saxon-HE Show documentation
Show all versions of Saxon-HE Show documentation
The XSLT and XQuery Processor
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2018-2023 Saxonica Limited
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
package net.sf.saxon.expr;
import net.sf.saxon.Configuration;
import net.sf.saxon.event.Outputter;
import net.sf.saxon.expr.elab.*;
import net.sf.saxon.expr.flwor.OuterForExpression;
import net.sf.saxon.expr.instruct.Choose;
import net.sf.saxon.expr.instruct.TailCall;
import net.sf.saxon.expr.parser.*;
import net.sf.saxon.lib.Feature;
import net.sf.saxon.om.*;
import net.sf.saxon.trace.ExpressionPresenter;
import net.sf.saxon.trans.UncheckedXPathException;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.type.*;
import net.sf.saxon.value.Cardinality;
import net.sf.saxon.value.IntegerValue;
import net.sf.saxon.value.SequenceType;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
/**
* A ForExpression maps an expression over a sequence.
* We use a ForExpression in preference to a FLWORExpression to handle simple cases
* (roughly, the XPath subset). In 9.6, we no longer convert a FLWORExpression to a ForExpression
* if there is a position variable, which simplifies the cases this class has to handle.
*/
public class ForExpression extends Assignation {
private int actionCardinality = StaticProperty.ALLOWS_MANY;
/**
* Create a "for" expression (for $x at $p in SEQUENCE return ACTION)
*/
public ForExpression() {
}
/**
* Get a name identifying the kind of expression, in terms meaningful to a user.
*
* @return a name identifying the kind of expression, in terms meaningful to a user.
* The name will always be in the form of a lexical XML QName, and should match the name used
* in explain() output displaying the expression.
*/
@Override
public String getExpressionName() {
return "for";
}
/**
* Type-check the expression
*/
/*@NotNull*/
@Override
public Expression typeCheck(ExpressionVisitor visitor, ContextItemStaticInfo contextInfo) throws XPathException {
// The order of events is critical here. First we ensure that the type of the
// sequence expression is established. This is used to establish the type of the variable,
// which in turn is required when type-checking the action part.
getSequenceOp().typeCheck(visitor, contextInfo);
if (Literal.isEmptySequence(getSequence()) && !(this instanceof OuterForExpression)) {
return getSequence();
}
if (requiredType != null) {
// if declaration is null, we've already done the type checking in a previous pass
SequenceType decl = requiredType;
SequenceType sequenceType = SequenceType.makeSequenceType(
decl.getPrimaryType(), StaticProperty.ALLOWS_ZERO_OR_MORE);
Supplier role =
() -> new RoleDiagnostic(RoleDiagnostic.VARIABLE, variableName.getDisplayName(), 0);
setSequence(TypeChecker.strictTypeCheck(
getSequence(), sequenceType, role, visitor.getStaticContext()));
ItemType actualItemType = getSequence().getItemType();
refineTypeInformation(actualItemType,
getRangeVariableCardinality(),
null,
getSequence().getSpecialProperties(), this);
}
if (Literal.isEmptySequence(getAction())) {
return getAction();
}
getActionOp().typeCheck(visitor, contextInfo);
actionCardinality = getAction().getCardinality();
return this;
}
/**
* Get the cardinality of the range variable
*
* @return the cardinality of the range variable (StaticProperty.EXACTLY_ONE). Can be overridden
* in a subclass
*/
protected int getRangeVariableCardinality() {
return StaticProperty.EXACTLY_ONE;
}
/**
* Optimize the expression
*/
/*@NotNull*/
@Override
public Expression optimize(ExpressionVisitor visitor, ContextItemStaticInfo contextItemType) throws XPathException {
Configuration config = visitor.getConfiguration();
Optimizer opt = visitor.obtainOptimizer();
boolean debug = config.getBooleanProperty(Feature.TRACE_OPTIMIZER_DECISIONS);
// Try to promote any WHERE clause appearing immediately within the FOR expression
if (Choose.isSingleBranchChoice(getAction())) {
getActionOp().optimize(visitor, contextItemType);
}
Expression p = promoteWhereClause();
if (p != null) {
if (debug) {
opt.trace("Promoted where clause in for $" + getVariableName(), p);
}
return p.optimize(visitor, contextItemType);
}
Expression seq0 = getSequence();
getSequenceOp().optimize(visitor, contextItemType);
if (seq0 != getSequence()) {
// if it changed, re-optimize
return optimize(visitor, contextItemType);
}
if (Literal.isEmptySequence(getSequence()) && !(this instanceof OuterForExpression)) {
return getSequence();
}
Expression act0 = getAction();
getActionOp().optimize(visitor, contextItemType);
if (act0 != getAction()) {
// it's now worth re-attempting the "where" clause optimizations
return optimize(visitor, contextItemType);
}
if (Literal.isEmptySequence(getAction())) {
return getAction();
}
// Simplify an expression of the form "for $b in a/b/c return $b/d".
// (XQuery users seem to write these a lot!)
if (getSequence() instanceof SlashExpression && getAction() instanceof SlashExpression) {
SlashExpression path2 = (SlashExpression) getAction();
Expression start2 = path2.getSelectExpression();
Expression step2 = path2.getActionExpression();
if (start2 instanceof VariableReference && ((VariableReference) start2).getBinding() == this &&
ExpressionTool.getReferenceCount(getAction(), this, false) == 1 &&
((step2.getDependencies() & (StaticProperty.DEPENDS_ON_POSITION | StaticProperty.DEPENDS_ON_LAST)) == 0)) {
Expression newPath = new SlashExpression(getSequence(), path2.getActionExpression());
ExpressionTool.copyLocationInfo(this, newPath);
newPath = newPath.simplify().typeCheck(visitor, contextItemType);
if (newPath instanceof SlashExpression) {
// if not, it has been wrapped in a DocumentSorter or Reverser, which makes it ineligible.
// see test qxmp299, where this condition isn't satisfied
if (debug) {
opt.trace("Collapsed return clause of for $" + getVariableName() +
" into path expression", newPath);
}
return newPath.optimize(visitor, contextItemType);
}
}
}
// Simplify an expression of the form "for $x in EXPR return $x". These sometimes
// arise as a result of previous optimization steps.
if (getAction() instanceof VariableReference && ((VariableReference) getAction()).getBinding() == this) {
if (debug) {
opt.trace("Collapsed redundant for expression $" + getVariableName(), getSequence());
}
return getSequence();
}
// If the cardinality of the sequence is exactly one, rewrite as a LET expression
if (getSequence().getCardinality() == StaticProperty.EXACTLY_ONE) {
LetExpression let = new LetExpression();
let.setVariableQName(variableName);
let.setRequiredType(SequenceType.makeSequenceType(
getSequence().getItemType(),
StaticProperty.EXACTLY_ONE));
let.setSequence(getSequence());
let.setAction(getAction());
let.setSlotNumber(slotNumber);
let.setRetainedStaticContextLocally(getRetainedStaticContext());
ExpressionTool.rebindVariableReferences(getAction(), this, let);
return let.typeCheck(visitor, contextItemType).optimize(visitor, contextItemType);
}
return this;
}
/**
* Replace this expression by a simpler expression that delivers the results without regard
* to order.
*
* @param retainAllNodes set to true if the result must contain exactly the same nodes as the
* original; set to false if the result can eliminate (or introduce) duplicates.
* @param forStreaming set to true if optimizing for streaming
*/
@Override
public Expression unordered(boolean retainAllNodes, boolean forStreaming) throws XPathException {
setSequence(getSequence().unordered(retainAllNodes, forStreaming));
setAction(getAction().unordered(retainAllNodes, forStreaming));
return this;
}
/**
* For an expression that returns an integer or a sequence of integers, get
* a lower and upper bound on the values of the integers that may be returned, from
* static analysis. The default implementation returns null, meaning "unknown" or
* "not applicable". Other implementations return an array of two IntegerValue objects,
* representing the lower and upper bounds respectively. The values
* UNBOUNDED_LOWER and UNBOUNDED_UPPER are used by convention to indicate that
* the value may be arbitrarily large. The values MAX_STRING_LENGTH and MAX_SEQUENCE_LENGTH
* are used to indicate values limited by the size of a string or the size of a sequence.
*
* @return the lower and upper bounds of integer values in the result, or null to indicate
* unknown or not applicable.
*/
/*@Nullable*/
@Override
public IntegerValue[] getIntegerBounds() {
return getAction().getIntegerBounds();
}
/**
* Promote a WHERE clause whose condition doesn't depend on the variable being bound.
* This rewrites an expression of the form
* let $i := SEQ return if (C) then R else ()
* to the form:
* if (C) then (let $i := SEQ return R) else ()
*
* @return an expression in which terms from the WHERE clause that can be extracted have been extracted
*/
/*@Nullable*/
private Expression promoteWhereClause() {
if (Choose.isSingleBranchChoice(getAction())) {
Expression condition = ((Choose) getAction()).getCondition(0);
Binding[] bindingList = new Binding[]{this};
List list = new ArrayList<>(5);
Expression promotedCondition = null;
BooleanExpression.listAndComponents(condition, list);
for (int i = list.size() - 1; i >= 0; i--) {
Expression term = list.get(i);
if (!ExpressionTool.dependsOnVariable(term, bindingList)) {
if (promotedCondition == null) {
promotedCondition = term;
} else {
promotedCondition = new AndExpression(term, promotedCondition);
}
list.remove(i);
}
}
if (promotedCondition != null) {
if (list.isEmpty()) {
// the whole if() condition has been promoted
Expression oldThen = ((Choose) getAction()).getAction(0);
setAction(oldThen);
return Choose.makeConditional(promotedCondition, this);
} else {
// one or more terms of the if() condition have been promoted
Expression retainedCondition = list.get(0);
for (int i = 1; i < list.size(); i++) {
retainedCondition = new AndExpression(retainedCondition, list.get(i));
}
((Choose) getAction()).setCondition(0, retainedCondition);
Expression newIf = Choose.makeConditional(
promotedCondition, this, Literal.makeEmptySequence());
ExpressionTool.copyLocationInfo(this, newIf);
return newIf;
}
}
}
return null;
}
/**
* Copy an expression. This makes a deep copy.
*
* @param rebindings variables that need to be re-bound
* @return the copy of the original expression
*/
/*@NotNull*/
@Override
public Expression copy(RebindingMap rebindings) {
ForExpression forExp = new ForExpression();
ExpressionTool.copyLocationInfo(this, forExp);
forExp.setRequiredType(requiredType);
forExp.setVariableQName(variableName);
forExp.setSequence(getSequence().copy(rebindings));
rebindings.put(this, forExp);
Expression newAction = getAction().copy(rebindings);
forExp.setAction(newAction);
forExp.variableName = variableName;
forExp.slotNumber = slotNumber;
return forExp;
}
/**
* Mark tail function calls: only possible if the for expression iterates zero or one times.
* (This arises in XSLT/XPath, which does not have a LET expression, so FOR gets used instead)
*/
@Override
public int markTailFunctionCalls(StructuredQName qName, int arity) {
if (!Cardinality.allowsMany(getSequence().getCardinality())) {
return ExpressionTool.markTailFunctionCalls(getAction(), qName, arity);
} else {
return UserFunctionCall.NOT_TAIL_CALL;
}
}
/**
* Determine whether this is a vacuous expression as defined in the XQuery update specification
*
* @return true if this expression is vacuous
*/
@Override
public boolean isVacuousExpression() {
return getAction().isVacuousExpression();
}
/**
* An implementation of Expression must provide at least one of the methods evaluateItem(), iterate(), or process().
* This method indicates which of these methods is provided. This implementation provides both iterate() and
* process() methods natively.
*/
@Override
public int getImplementationMethod() {
return ITERATE_METHOD | PROCESS_METHOD;
}
/**
* Check that any elements and attributes constructed or returned by this expression are acceptable
* in the content model of a given complex type. It's always OK to say yes, since the check will be
* repeated at run-time. The process of checking element and attribute constructors against the content
* model of a complex type also registers the type of content expected of those constructors, so the
* static validation can continue recursively.
*/
@Override
public void checkPermittedContents(SchemaType parentType, boolean whole) throws XPathException {
getAction().checkPermittedContents(parentType, false);
}
/**
* Iterate over the sequence of values
*/
/*@NotNull*/
@Override
public SequenceIterator iterate(XPathContext context) throws XPathException {
// First create an iteration of the base sequence.
// Then create a MappingIterator which applies a mapping function to each
// item in the base sequence. The mapping function is essentially the "return"
// expression, wrapped in a MappingAction object that is responsible also for
// setting the range variable at each step.
SequenceIterator base = getSequence().iterate(context);
MappingAction map = new MappingAction(context, getLocalSlotNumber(), getAction());
switch (actionCardinality) {
case StaticProperty.EXACTLY_ONE:
return new ItemMappingIterator(base, map, true);
case StaticProperty.ALLOWS_ZERO_OR_ONE:
return new ItemMappingIterator(base, map, false);
default:
return new MappingIterator(base, map);
}
}
/**
* Process this expression as an instruction, writing results to the current
* outputter
*/
@Override
public void process(Outputter output, XPathContext context) throws XPathException {
dispatchTailCall(makeElaborator().elaborateForPush().processLeavingTail(output, context));
}
/**
* Determine the data type of the items returned by the expression, if possible
*
* @return one of the values Type.STRING, Type.BOOLEAN, Type.NUMBER, Type.NODE,
* or Type.ITEM (meaning not known in advance)
*/
/*@NotNull*/
@Override
public ItemType getItemType() {
return getAction().getItemType();
}
/**
* Get the static type of the expression as a UType, following precisely the type
* inference rules defined in the XSLT 3.0 specification.
*
* @param contextItemType the static type of the context item
* @return the static item type of the expression according to the XSLT 3.0 defined rules
*/
@Override
public UType getStaticUType(UType contextItemType) {
return getAction().getStaticUType(contextItemType);
}
/**
* Determine the static cardinality of the expression
*/
@Override
protected int computeCardinality() {
int c1 = getSequence().getCardinality();
int c2 = getAction().getCardinality();
return Cardinality.multiply(c1, c2);
}
/**
* The toString() method for an expression attempts to give a representation of the expression
* in an XPath-like form, but there is no guarantee that the syntax will actually be true XPath.
* In the case of XSLT instructions, the toString() method gives an abstracted view of the syntax
*
* @return a representation of the expression as a string
*/
public String toString() {
return "for $" + getVariableEQName() +
allowingEmptyString() +
" in " + (getSequence() == null ? "(...)" : getSequence().toString()) +
" return " + (getAction() == null ? "(...)" : ExpressionTool.parenthesize(getAction()));
}
@Override
public String toShortString() {
return "for $" + getVariableQName().getDisplayName() +
allowingEmptyString() +
" in " + (getSequence() == null ? "(...)" : getSequence().toShortString()) +
" return " + (getAction() == null ? "(...)" : getAction().toShortString());
}
protected String allowingEmptyString() {
return "";
}
/**
* Diagnostic print of expression structure. The abstract expression tree
* is written to the supplied output destination.
*/
@Override
public void export(ExpressionPresenter out) throws XPathException {
out.startElement("for", this);
explainSpecializedAttributes(out);
out.emitAttribute("var", getVariableQName());
ItemType varType = getSequence().getItemType();
if (varType != AnyItemType.getInstance()) {
out.emitAttribute("as", AlphaCode.fromItemType(varType));
}
out.emitAttribute("slot", "" + getLocalSlotNumber());
out.setChildRole("in");
getSequence().export(out);
out.setChildRole("return");
getAction().export(out);
out.endElement();
}
protected void explainSpecializedAttributes(ExpressionPresenter out) {
// no action
}
/**
* The MappingAction represents the action to be taken for each item in the
* source sequence. It acts as the MappingFunction for the mapping iterator.
*/
public static class MappingAction implements MappingFunction, ItemMappingFunction {
protected XPathContext context;
private final int slotNumber;
private final Expression action;
public MappingAction(XPathContext context,
int slotNumber,
Expression action) {
this.context = context;
this.slotNumber = slotNumber;
this.action = action;
}
/*@Nullable*/
public SequenceIterator map(Item item) throws XPathException {
context.setLocalVariable(slotNumber, item);
return action.iterate(context);
}
/*@Nullable*/
public Item mapItem(Item item) throws XPathException {
context.setLocalVariable(slotNumber, item);
return action.evaluateItem(context);
}
}
@Override
public String getStreamerName() {
return "ForExpression";
}
/**
* Make an elaborator for this expression
*
* @return a suitable elaborator
*/
@Override
public Elaborator getElaborator() {
return new ForExprElaborator();
}
/**
* An elaborator for a "for" expression, typically written as {for $x in SEQ return R}.
*
* Provides both "pull" and "push" implementations.
*/
public static class ForExprElaborator extends PullElaborator {
public PullEvaluator elaborateForPull() {
final ForExpression expr = (ForExpression) getExpression();
final PullEvaluator selectEval = expr.getSequence().makeElaborator().elaborateForPull();
final int actionCardinality = expr.getAction().getCardinality();
final int slot = expr.getLocalSlotNumber();
if (Cardinality.allowsMany(actionCardinality)) {
final PullEvaluator actionEval = expr.getAction().makeElaborator().elaborateForPull();
return context -> {
SequenceIterator base = selectEval.iterate(context);
return new MappingIterator(base, item -> {
context.setLocalVariable(slot, item);
return actionEval.iterate(context);
});
};
} else {
final ItemEvaluator actionEval = expr.getAction().makeElaborator().elaborateForItem();
return context -> {
SequenceIterator base = selectEval.iterate(context);
return new ItemMappingIterator(base, item -> {
context.setLocalVariable(slot, item);
return actionEval.eval(context);
});
};
}
}
@Override
public PushEvaluator elaborateForPush() {
final ForExpression expr = (ForExpression) getExpression();
final PullEvaluator selectEval = expr.getSequence().makeElaborator().elaborateForPull();
final PushEvaluator actionEval = expr.getAction().makeElaborator().elaborateForPush();
final int slot = expr.getLocalSlotNumber();
return (out, context) -> {
SequenceIterator base = selectEval.iterate(context);
for (Item item; (item = base.next()) != null; ) {
context.setLocalVariable(slot, item);
TailCall tc = actionEval.processLeavingTail(out, context);
dispatchTailCall(tc);
}
return null;
};
}
@Override
public UpdateEvaluator elaborateForUpdate() {
final ForExpression expr = (ForExpression) getExpression();
final PullEvaluator selectEval = expr.getSequence().makeElaborator().elaborateForPull();
final UpdateEvaluator actionEval = expr.getAction().makeElaborator().elaborateForUpdate();
final int slot = expr.getLocalSlotNumber();
return (context, pul) -> {
try {
SequenceTool.supply(selectEval.iterate(context), (ItemConsumer super Item>) item -> {
context.setLocalVariable(slot, item);
actionEval.registerUpdates(context, pul);
});
} catch (UncheckedXPathException e) {
throw e.getXPathException();
}
};
}
}
}