Bug #5560


intersect doesn’t seem to work correctly for attributes

Added by Gerrit Imsieke 2 months ago. Updated about 2 months ago.

Start date:
Due date:
% Done:


Estimated time:
Applies to JS Branch:
1.0, 2
Fix Committed on JS Branch:
Fixed in JS Release:
SEF Generated with:
Contact person:
Additional contact persons:


While developping XPathle, I stumbled across SaxonJS behaviour that was different than in the Java version.

Consider the commented-out code at

If you use the commented-out lines that contain intersect instead of the preceding two xsl:when ⁠s and recompile the SEF and use the example “XSLT 2.0 for postprocessing a CSS parse tree” and enter, for example, //@as as your guess, the as attributes will not be highlighted.

Comparing generated IDs fixed it for me, and this can immediately be tested on

This issue hit me with the v1.2 SEF that Saxon EE 9.9 generated, and I therefore purchased and used Saxon EE 11 for generating an .sef.json, which unfortunately didn’t fix the issue. (I don’t regret buying a new EE version though.)


sample2.xml (68 Bytes) sample2.xml Martin Honnen, 2022-06-16 10:47
sheet2.xsl (814 Bytes) sheet2.xsl Martin Honnen, 2022-06-16 10:47
sheet2.xsl.saxoncs-export.sef.json (2.93 KB) sheet2.xsl.saxoncs-export.sef.json Martin Honnen, 2022-06-16 10:47
sheet2.xsl.saxonjs-export.sef.json (6.38 KB) sheet2.xsl.saxonjs-export.sef.json Martin Honnen, 2022-06-16 10:47
Actions #2

Updated by Norm Tovey-Walsh about 2 months ago

  • Priority changed from Low to High
  • Sprint/Milestone set to SaxonJS 3.0
Actions #3

Updated by Martin Honnen about 2 months ago

I have tried to understand what could go wrong, I have not been able to identify what goes wrong, but so far all my attempts suggest that the problem only occurs if SaxonJS runs a SaxonEE exported stylesheet sef.json stylesheet. On the fly or with a SaxonJS/XX compiler generated sef.json stylesheet the problem does not occur.

Sample stylesheet is:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl=""

  <xsl:mode on-no-match="shallow-copy"/>
  <xsl:param name="guess-path" select="//@id"/>

  <xsl:template match="@*">
    <xsl:if test=". intersect $guess-path">
      <xsl:attribute name="highlight">test</xsl:attribute>
  <xsl:template match="/" name="xsl:initial-template">
    <xsl:comment>Run with {system-property('xsl:product-name')} {system-property('xsl:product-version')} {system-property('Q{}platform')}</xsl:comment>

Now when I run it directly or with xslt3 (of SaxonJS 2.4) compiled sef.json through xslt3 it gives the right output:

<?xml version="1.0" encoding="UTF-8"?><root id="el1" highlight="test" class="foo">
  <item id="el2" highlight="test" class="bar"/>
</root><!--Run with SaxonJS 2.4 Node.js-->

However, when I use SaxonCS or EE to compile for SaxonJS, I get the wrong output:

<?xml version="1.0" encoding="UTF-8"?><root id="el1" class="foo">
  <item id="el2" class="bar"/>
</root><!--Run with SAXON JS 11.3 Node.js-->

Input sample is e.g.

<root id="el1" class="foo">
  <item id="el2" class="bar"/>

I am afraid I am not skilled enough to identify whether the SaxonCS/EE generated sef.json is wrong (it doesn't seem to contain an intersect, for instance) or whether the SaxonJS runtime can't handle the perhaps optimized SaxonCS/EE compiled sef.json for that sample.

Actions #4

Updated by Gerrit Imsieke about 2 months ago

Thanks Martin for investigating and for preparing a minimal test case!

Actions #5

Updated by Martin Honnen about 2 months ago

SaxonCS/EE seems to compile the intersect check into something like exists(among(..)) while SaxonJS seems to compile intersect in the XSLT/XPath into intersect in the SEF as well. But I have now tested that a template matching element nodes instead of attribute nodes and using the same intersect test gives the same SEF compilation with exists(among(..)) with SaxonCS/EE but that SaxonJS executes that test fine with element nodes. The SaxonJS among function in the end seems to do === which I think, in terms of JavaScript objects like nodes should do an identity check but from the minimized/garbled source I am not able to see/figure what kind of arguments are processed.

Actions #6

Updated by Martin Honnen about 2 months ago

In terms of among with element nodes it indeed seems that two DOM element nodes end up being compared with ===, but as SaxonJS seems to handle attribute nodes differently it appears some wrapper/closure around the node ends up being compared and these wrappers/closures, even if having the same attribute node (data) inside (e.g. id="el1") compared with === are not the same objects so the comparison returns false. That seems to be basically the reason why the SaxonCS/EE compiled test=". intersect $nodes" fails with . being attribute node and attribute nodes being in $nodes, it does some comparison with among doing some === comparison failing to identify identical attributes as not a DOM attribute node (reference) is compared with another DOM attribute node reference but two SaxonJS data structures that are not the same object but reference the same attribute fail the === comparison.

Actions #7

Updated by Martin Honnen about 2 months ago

Replacing the code in among doing return S===T with return S instanceof Node ? S===T : S.parent === T.parent && === && S.namespaceURI === T.namespaceURI} seems to fix the attribute handling. But I have no idea whether it breaks other things or which other type of wrappers for which the second part after the colon doesn't make sense could be passed to the function.

Actions #8

Updated by Michael Kay about 2 months ago

The basic problem is that we are using a === b to compare nodes for identity, and this doesn't work for attributes because the attribute axis constructs attribute nodes "on the fly". We should be using DomUtils.isSameNode(a, b).

The "among" operator is basically an optimisation for a one-to-many intersect. This is most commonly encountered in a boolean context

if (X intersect (P, Q, R))

and the main benefit of the optimisation is to avoid sorting nodes into document order. The XJ compiler does this optimisation; XX does not.

Actions #9

Updated by Michael Kay about 2 months ago

Come to think of it, the code for "among" has another bug. If we don't sort the sequence into document order, and don't eliminate duplicates, there's a danger that (P intersect (P, Q, R, P)) is going to return (P, P). rather than (P).

Added a QT3 test case for this: -s:op-intersect -t:K2-SeqIntersect-48

SaxonJ is passing the test because it effectively rewrites the expression as

((P, Q, R, P)[. is P][1])

That is, it knows the expression can return at most one node so it stops processing the input after one node has been found.

Actions #10

Updated by Michael Kay about 2 months ago

I'm inclined to fix this by scrapping the SingletonIntersectExpr ("among" operator) in SaxonJ, rewriting instead as

let $x := §ARG0§ return head(§ARG1§[. is $x])

and dropping support for "among" in SaxonJS 3 (SEF files will probably need to be recompiled for SaxonJS 3 anyway).

Reducing the number of instructions is always good, as each instruction now carries a lot of code and potential bugs (bytecode, streaming, etc).

Please register to edit this issue

Also available in: Atom PDF Tracking page