Bug #5092

"java.lang.AssertionError: **** Component reference mode xsl:unnamed is already bound" when using mode #all and COMPILE_WITH_TRACING == true

Added by Maarten Kroon about 1 month ago. Updated about 1 month ago.

Start date:
Due date:
% Done:


Estimated time:
Legacy ID:
Applies to branch:
10, trunk
Fix Committed on Branch:
10, trunk
Fixed in Maintenance Release:



When I run the attached sample transformation "mode-all-test.xsl" on "source.xml" the following exception is thrown:

java.lang.AssertionError: **** Component reference mode xsl:unnamed is already bound 

The error only occurs when:

  • "compileWithTracing" is set to true in config.xml
  • The template contains a mode="#all"
  • A mode declaration exists for another mode
  • Saxon version 10.5 is used (no errors on version 10.3)

I run the transformation using the following command:

java -jar saxon-he-10.5.jar -s:source.xml -xsl:mode-all-test.xsl -config:config.xml

Kind regards, Maarten

source.xml (47 Bytes) source.xml Maarten Kroon, 2021-09-15 13:15
mode-all-test.xsl (295 Bytes) mode-all-test.xsl Maarten Kroon, 2021-09-15 13:15
config.xml (174 Bytes) config.xml Maarten Kroon, 2021-09-15 13:15
stacktrace.txt (1.59 KB) stacktrace.txt Maarten Kroon, 2021-09-15 13:16


#1 Updated by Michael Kay about 1 month ago

Thanks for reporting it. Problem reproduced.

#2 Updated by Michael Kay about 1 month ago

When a template rule is in more than one mode, we should be making multiple copies of the rule, because it may behave differently in different modes. We have made multiple copies of the Rule and TemplateRule objects, but they share the same body, which (because tracing is enabled) is in this case a ComponentTracer instruction. I need to establish why the ComponentTracer has not been copied.

(The failure to copy it means that allocating binding slots for the apply-templates instruction in one mode would be overwritten by the binding slots allocated for the other mode; but we have a check to prevent this happening, and it's this check that causes the error message.)

#3 Updated by Michael Kay about 1 month ago

This is rather complicated...

We have a hash table keyed on mode name, each entry being the list of template rules in that mode.

With a single template rule having mode="#all", given that there are two "real" modes in the stylesheet (test and #unnamed), this table ends up with three entries, for the modes (#all, temp, #unnamed), each containing a single template rule. For modes #all and #unnamed, the entries contain the same template rule R1; for mode temp, the entry contains a copy of R1, say R2. R2 is marked as being a "slave" copy of R1. (See XSLTemplate#921).

In method XSLTemplate.compileTemplateRule() (line 747), we process all the entries in this table. For all but the first, we copy the template body. We then update slave copies of the rule, and finally we inject tracing code (assuming tracing was requested). The way this works in our case is:

(1) process the entry with mode="#unnamed", template rule R1. This is the first, so the body (ApplyT1645) is not copied.

(2) update slave copies of R1. There is one such copy (R2). R2 currently has no body; this action causes it to acquire a copy of the body of R1.

(3) inject trace code into the body of R1

(4) process the entry with mode="#all". This is again template rule R1. This is not the first in the list, so the body is copied. But in fact, what we copy is not the body of R1, but the content of the variable body, which is the old body of R1 before trace code injection. So R1 (which is used in both #unnamed and #all) is updated to a copy of the template body with no trace instructions.

(5) update slave copies of R1. This causes R2 to acquire a new copy of the body of R1, sans injected trace code.

(6) inject trace code into the body of R1 (which affects both #unnamed and #all, since they both reference R1)

(7) process the entry with mode="test". This is rule R2. We make another copy of the template rule body. Updating slave copies has no effect, because there are none. We then inject trace code.

By a very long-winded route, we now have three entries containing two copies of the template, all with trace code injected. Note that the order of entries in the hash table is unpredictable, so the outcome could possibly have been different. So far, although it looks inefficient, the outcome makes sense.

Later, we get to PrincipalStylesheetModule.allocateBindingSlots(), which processes the stylesheet one component at a time. There are two components, representing the modes test and #unnamed. Again the order is unpredictable, but we process test first. This takes us to TR1769->CT1770->TE1792->AT1799. We set the binding slot in AT1799 to 0.

Then we process mode #unnamed. This takes us to TR1628->CT1770. So the two template rule objects are distinct, but share the same body. How did this happen? Previously, they definitely had distinct bodies.

#4 Updated by Michael Kay about 1 month ago

Well, between the trace injection and the slot number allocation, there is another phase of processing: XSLTemplate.optimize().

This contains the suspicious lines:

Expression body = compiledTemplateRules.values().stream().findFirst().map(TemplateRule::getBody).orElse(null);
// Until now, all template rules share the same body.

which is explicitly assuming that the body of the first compiledTemplateRule can be used for all of them.

The optimize() method processes each of the three template rules (a list of three entries containing two R1s and an R2), and for each one it makes a copy (yet another), and performs type checking and optimization on this copy. Then for each TemplateRule, if calls getRules() to get the rules that use this TemplateRule and (surprisingly) finds there are two. For TR1636 (which it processes twice) it finds two rules R2132 and R2133; for TR1744 (which it processes once) it finds one rule R2605.

Somehow, during optimization processing of the third entry in the template rule list, the optimised template body gets assigned to both the template rules.

The problem appears to be that the ComponentTracer has a reference to its containing component, and when we copy the ComponentTracer, we update the containing component to point to the new ComponentTracer. But in this case, the ComponentTracer is pointing to the wrong containing component, as a result of the complex process by which the tracing code was injected, as described in comment #3.

#5 Updated by Michael Kay about 1 month ago

So, I think there are several things we need to do.

(1) we should get the #all dummy mode off the list of modes we process in XSLTemplate.compiledTemplateRules. Once the template rule has been copied to all the real modes, we can forget the original.

(2) we're copying the body of the template rule an excessive number of times.

(3) we should do trace code injection once, before we do any copying

(4) the ComponentTracer shouldn't be responsible for attaching itself to the containing component; that's the job of the containing component.

Rather a lot to emerge from the compilation of a 10-line stylesheet!

#6 Updated by Michael Kay about 1 month ago

  • Status changed from New to In Progress

I have made and am testing the following changes:

  1. In XSLTemplate.compileTemplateRule()

-- inject trace code into the body once, before making any copies of the body

-- avoid processing the mode="#all" version of the template, which is never executed

  1. In XSLTemplate.optimize()

-- avoid the redundant copy of the template body

-- avoid processing the mode="#all" version of the template, which is never executed

  1. In ComponentTracer

-- in the constructor, avoid modifying the parent component

-- in the copy() method, do the initialisation of the new copy explicitly rather than relying on the logic in the constructor

#7 Updated by Michael Kay about 1 month ago

These changes cause no regression in the XSLT3 tests.

It has to be said, however, that these tests aren't run with trace injection, and we don't have that many tests that exercise this. I think we used to have an option to run the whole test suite with dummy trace code injected; I'll see if that can be reinstated.

#8 Updated by Michael Kay about 1 month ago

We do in fact have the option to run the tests with a (long-forgotten) -T option to run with injected trace.

Because this hasn't been run for quite a while, it is of course failing. Currently on -s:iterate -t:iterate-033; for reasons that are probably entirely unrelated to this bug.

#9 Updated by Michael Kay about 1 month ago

  • Category set to Diagnostics
  • Status changed from In Progress to Resolved
  • Assignee set to Michael Kay
  • Priority changed from Low to Normal
  • Applies to branch trunk added
  • Fix Committed on Branch 10, trunk added

Fixed as described in comment #6, on the 10 and 11 branches. Regression tested by running the XSLT3 test suite with the -T option; this produced a number of failures which need to be investigated, but they are not caused by the patch.

#10 Updated by Maarten Kroon about 1 month ago

Thanks! I did not expect that the 10-line stylesheet would lead to such a complex investigation either ;-)

#11 Updated by O'Neil Delpratt about 1 month ago

  • Status changed from Resolved to Closed
  • % Done changed from 0 to 100
  • Fixed in Maintenance Release 10.6 added

Bug fix applied in the Saxon 10.6 maintenance release

Please register to edit this issue

Also available in: Atom PDF