@@ -187,6 +187,17 @@ public class XQueryContext implements BinaryValueManager, Context {
187187 // The last element in the linked list of local in-scope variables
188188 private LocalVariable lastVar = null ;
189189
190+ /**
191+ * O(1) lookup table from QName to the most-recently-declared LocalVariable
192+ * with that name. Maintained alongside the {@link #lastVar} linked list:
193+ * declareVariableBinding adds, popLocalVariables restores the prevSameName
194+ * chain. Visibility is enforced in resolveLocalVariable by comparing
195+ * {@link LocalVariable#markedUnder} to {@code contextStack.peek()}.
196+ */
197+ // Shared by reference with copies of this context (see copyFields,
198+ // updateContext) just like {@link #contextStack} and {@link #lastVar}.
199+ private Map <QName , LocalVariable > localVariableLookup = new HashMap <>();
200+
190201 private Deque <LocalVariable > contextStack = new ArrayDeque <>();
191202
192203 private final Deque <FunctionSignature > callStack = new ArrayDeque <>();
@@ -657,6 +668,7 @@ public void updateContext(final XQueryContext from) {
657668 this .watchdog = from .watchdog ;
658669 this .lastVar = from .lastVar ;
659670 this .contextStack = from .contextStack ;
671+ this .localVariableLookup = from .localVariableLookup ;
660672 this .inScopeNamespaces = from .inScopeNamespaces ;
661673 this .inScopePrefixes = from .inScopePrefixes ;
662674 this .inheritedInScopeNamespaces = from .inheritedInScopeNamespaces ;
@@ -727,6 +739,7 @@ protected void copyFields(final XQueryContext ctx) {
727739 ctx .lastVar = this .lastVar ;
728740 ctx .variableStackSize = getCurrentStackSize ();
729741 ctx .contextStack = this .contextStack ;
742+ ctx .localVariableLookup = this .localVariableLookup ;
730743 ctx .staticNamespaces = new HashMap <>(this .staticNamespaces );
731744 ctx .staticPrefixes = new HashMap <>(this .staticPrefixes );
732745
@@ -1459,6 +1472,7 @@ public void reset(final boolean keepGlobals) {
14591472
14601473 if (!isShared ) {
14611474 lastVar = null ;
1475+ localVariableLookup .clear ();
14621476 }
14631477
14641478 // clear inline functions using closures
@@ -1924,6 +1938,8 @@ public LocalVariable declareVariableBinding(final LocalVariable var) throws XPat
19241938 }
19251939 lastVar = var ;
19261940 var .setStackPosition (getCurrentStackSize ());
1941+ var .markedUnder = contextStack .peek ();
1942+ var .prevSameName = localVariableLookup .put (var .getQName (), var );
19271943 return var ;
19281944 }
19291945
@@ -2057,16 +2073,19 @@ Variable resolveGlobalVariable(final QName qname) {
20572073 }
20582074
20592075 protected Variable resolveLocalVariable (final QName qname ) throws XPathException {
2060- final LocalVariable end = contextStack .peek ();
2061- for (LocalVariable var = lastVar ; var != null ; var = var .before ) {
2062- if (var == end ) {
2063- return null ;
2064- }
2065- if (qname .equals (var .getQName ())) {
2066- return var ;
2067- }
2076+ // O(1) fast path. The linked-list walk previously here is O(N) per
2077+ // call and O(N²) when a body of N variables is analyzed.
2078+ final LocalVariable var = localVariableLookup .get (qname );
2079+ if (var == null ) {
2080+ return null ;
20682081 }
2069- return null ;
2082+ // Visibility: var is visible if it was declared under the current
2083+ // contextStack mark — the same boundary the linked-list walk used to
2084+ // express by stopping at {@code contextStack.peek()}.
2085+ if (var .markedUnder != contextStack .peek ()) {
2086+ return null ;
2087+ }
2088+ return var ;
20702089 }
20712090
20722091 /**
@@ -2513,10 +2532,27 @@ public void popLocalVariables(@Nullable final LocalVariable var) {
25132532 /**
25142533 * Restore the local variable stack to the position marked by variable var.
25152534 *
2535+ * <p>Walks {@link #lastVar} backward to {@code var} (or to the start when
2536+ * {@code var} is {@code null}), unwinding each variable's
2537+ * {@code prevSameName} chain into {@link #localVariableLookup} in
2538+ * REVERSE-of-declaration order so that names with multiple bindings in the
2539+ * popped scope settle on the still-visible binding, not on a popped one.
2540+ *
25162541 * @param var only clear variables after this variable, or null
25172542 * @param resultSeq the result sequence
25182543 */
25192544 public void popLocalVariables (@ Nullable final LocalVariable var , @ Nullable final Sequence resultSeq ) {
2545+ for (LocalVariable cursor = lastVar ; cursor != null && cursor != var ; cursor = cursor .before ) {
2546+ if (localVariableLookup .get (cursor .getQName ()) == cursor ) {
2547+ if (cursor .prevSameName != null ) {
2548+ localVariableLookup .put (cursor .getQName (), cursor .prevSameName );
2549+ } else {
2550+ localVariableLookup .remove (cursor .getQName ());
2551+ }
2552+ }
2553+ cursor .prevSameName = null ;
2554+ }
2555+
25202556 if (var != null ) {
25212557 // clear all variables registered after var. they should be out of scope.
25222558 LocalVariable outOfScope = var .after ;
0 commit comments