Skip to content

fix(#6669): atomize the auto-name before the fold checks - #6672

Open
yangjj-iso wants to merge 1 commit into
objectionary:masterfrom
yangjj-iso:6669
Open

fix(#6669): atomize the auto-name before the fold checks#6672
yangjj-iso wants to merge 1 commit into
objectionary:masterfrom
yangjj-iso:6669

Conversation

@yangjj-iso

Copy link
Copy Markdown

Closes #6669

runtime is red on master because of this, so nothing merges until it is green — including my own #6668, which is how I came to it.

Root cause

The stack trace on the issue names the caller: the template rule at inline-cactoos.xsl:314. That template hands @name — an attribute node — to nine functions in a row:

<xsl:if test="eo:recursive(., @name) or eo:dispatched(., @name) or ... or eo:reapplied(., @name)">

Every one of them declares its $name as xs:string, so the atomization was left to the function conversion rules. Saxon is free to bind a parameter to a closure over the argument expression rather than over its converted value, and then the node itself arrives inside the function. There the comparison

eo:resolved-name(@base) = $name

has already been compiled to a ValueComparison, on the strength of both operands being statically xs:string — so it casts straight to AtomicValue, and the sheet dies with DOMNodeWrapper cannot be cast to AtomicValue.

The other operand cannot be the node: eo:resolved-name builds its result with substring-after, so it is a string by construction. $name is the only candidate, and that template is the only place a node can reach it — every other call site passes the $name of the inlining template at line 100, which is itself eo:resolved-name(@base).

This also accounts for the "moved the crash" reading in the issue. count() materialised the sequence inside the function's own evaluation; [2] reaches it through a lazy subscript, which is exactly the context where the deferred conversion is lost. #6638 did not introduce the type mismatch — it changed the evaluation strategy enough to expose it.

The change

Atomize once, in the template, before any of the nine is asked:

<xsl:variable name="name" as="xs:string" select="string(@name)"/>

Two lines. It leaves the conversion nothing to lose and covers all nine functions at once, rather than rewriting the comparison inside each of them — the same move as the eo:signature call in to-eo-tree, and as the @local wrap in restore-local-names for #6650.

string() is exact here rather than merely close: the match pattern is o[starts-with(@name, $auto) and not(eo:void(.))], so @name is always present, and the parameters are required xs:string. Nothing about what the nine functions are asked changes.

Verification

The failure is optimiser-dependent and does not reproduce on demand — that is the shape of the bug, and it is why I have no regression test to offer. Master is red on runtime (23) while the same sources print canonically on the other runners.

So what I can show is that the change is inert on everything that does run:

master with this change
mvn -pl eo-printer test 346 passed 346 passed
mvn -pl eo-runtime process-sources all 170 sources canonical all 170 sources canonical
transpiled 170 XMIRs, 179 Java files 170 XMIRs, 179 Java files

Both columns are this branch's base, d421d8d; the left one was produced by stashing the change and reinstalling eo-printer, so the comparison is like for like.

What I deliberately left alone

restore-local-names.xsl has the same shape — eo:recursive(., @name) and five others, called from match patterns — and it took the same [2] change in 28e7534. I did not touch it here: its parameters are xs:string? rather than xs:string, so string(@name) would turn an absent @name from an empty sequence into '' and change what those functions answer. The faithful form there is @name/string(), which deserves its own change and its own reasoning rather than riding along with this one. Glad to open it if you want it.

… checks

The drop template handed "@name" - an attribute node - to nine functions
that each declare their "$name" as "xs:string", leaving the conversion to
the function conversion rules. Saxon is free to bind a parameter to a
closure over the argument expression rather than over its converted value,
and the node then arrives inside the function. There
"eo:resolved-name(@base) = $name" has already been compiled to a
"ValueComparison" on the strength of the declared types, so it casts
straight to "AtomicValue" and the sheet dies with "DOMNodeWrapper cannot
be cast to AtomicValue".

That template is the only place a node can enter "$name". Every other call
site passes the "$name" of the inlining template, which "eo:resolved-name"
builds with "substring-after" and which therefore cannot be anything but a
string. Atomizing once, in the template, leaves the conversion nothing to
lose and covers all nine functions at once, rather than rewriting the
comparison in each of them - the same move as the "eo:signature" call in
"to-eo-tree" and the "@Local" wrap in "restore-local-names".

It also explains why objectionary#6638 appeared to move the crash: "[2]" evaluates the
predicate through a lazy subscript, which is precisely the context in which
the deferred conversion is lost, where "count()" had materialised the
sequence inside the call.

The failure is optimiser-dependent and does not reproduce on demand, so
there is no test to add: the runtime workflow is red on master while the
same sources print canonically on other runners. Before and after this
change "mvn -pl eo-runtime process-sources" reports all 170 sources
formatted canonically here, transpiling to the same 179 Java files, and the
346 tests of eo-printer pass. The change removes the possibility of the
crash rather than fixing a reproducible one.

Co-authored-by: Cursor <cursoragent@cursor.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a nondeterministic Saxon optimizer-dependent crash in the EO printer’s inline-cactoos.xsl by ensuring that the auto-generated @name attribute is atomized to an xs:string before it is passed into multiple xs:string-typed helper functions used in fold/inline checks.

Changes:

  • Atomizes @name once via string(@name) into an xs:string variable before running the chain of fold-check predicates.
  • Updates the predicate calls to pass the atomized $name instead of the @name attribute node.
  • Adds explanatory documentation describing the Saxon ValueComparison / deferred-conversion failure mode and rationale for the fix.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@yangjj-iso yangjj-iso closed this Aug 12, 2026
@yangjj-iso yangjj-iso reopened this Aug 12, 2026
@sonarqubecloud

Copy link
Copy Markdown

@yangjj-iso

Copy link
Copy Markdown
Author

Reopened to re-run CI against current master. Nothing about the branch changed.

The first runtime (23) here was cancelled at the timeout, not failed. The two are worth separating, because the checks list shows them the same way and they mean opposite things:

  • the crash this PR is about fails fast — between 3.5 and 9 minutes on every run that hit it today;
  • the hang fails slow — cancelled around the 25 minute mark, which is how most of today's runtime runs ended, on master and on every branch.

This branch is based on d421d8d, before #6671 landed, so it drew the second one and ran 32 minutes before the runner collected it. Its first Maven step — which also builds eo-runtime and runs format — passed, so the crash did not recur there. Master's own runtime then went green in 13.5 minutes on 2e9427b, immediately after #6671 merged, and that is the first clean run of that job today.

A pull_request run uses the merge of this branch with the base, so reopening picks up #6671 without touching the branch. This time the job should get far enough to say something about the change itself.

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

🚀 Performance Analysis

All benchmarks are within the acceptable range. No critical degradation detected (threshold is 100%). Please refer to the detailed report for more information.

Click to see the detailed report
Test Base Score PR Score Change % Change Unit Mode
benchmarks.XslBench.manySheetsOnLargeXmir 831.324 834.847 3.523 0.42% ms/op Average Time

⚠️ Performance loss: benchmarks.XslBench.manySheetsOnLargeXmir is slower by 3.523 ms/op (0.42%)

@github-actions

Copy link
Copy Markdown
Contributor

These counts changed in this branch,
while the lower each one of them, the better:

Metric master branch change
statics 604 605 +1

They are defined in .github/workflows/counts.yml

@github-actions github-actions Bot added the counts Some counts changed in this PR label Aug 12, 2026
@yegor256

Copy link
Copy Markdown
Member

@yangjj-iso conflicts here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

counts Some counts changed in this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

inline-cactoos.xsl crashes with a ClassCastException in eo:multi-referenced — the [2] fix for #6638 moved the crash instead of removing it

3 participants