This is an automated email from the ASF dual-hosted git repository.

slawrence pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/daffodil.git


The following commit(s) were added to refs/heads/main by this push:
     new f3fb555c9 Reduce copies in VariableMap implementation
f3fb555c9 is described below

commit f3fb555c98aa3cfe24270f38b579b03316cc8ad0
Author: Steve Lawrence <[email protected]>
AuthorDate: Thu Oct 19 11:25:21 2023 -0400

    Reduce copies in VariableMap implementation
    
    The VariableMap currently uses a Scala Map to look up variable
    instances, where the key is the QName of the variable and the value is
    the variable instance stack.
    
    One consideration with the data structure to be used is that it must be
    frequently copied each time a suspension is created. So although fast
    lookups are important, so are fast copies. And unfortunately, Map have
    relatively expensive copies. A large backing array must be created,
    buckets allocated, keys must be rehashed, etc. This overhead is enough
    that it can be a significant contribution when profiling, especially in
    formats with lots of suspensions.
    
    To fix this, this replaces the Map with an Array, and each
    VariableRuntimeData is assigned an index into this array. Now, instead
    of looking up a variable by its QName, we just get its index from the
    VRD and access that index in the array. This has the same constant time
    lookup, but array copies are faster and require fewer and smaller
    allocations.
    
    Similarly, during suspensions we must also create an immutable copy of
    each variable instance inside the array so that suspensions do not see
    new variables instances. Variable instances are currently represented as
    an ArrayBuffer, which has noticeable overhead when copying. Since this
    ArrayBuffer is just used as a stack, this replaces it with a Seq. This
    gives us similar stack-like behavior, but is immutable so copies are
    free. This does mean however that new Seq's must be allocated if we
    create a lot of newVariableInstances, but that is rare enough, and
    still fairly fast, that the benefit of preallocating an ArrayBuffer
    stack does not have significant benefits.
    
    Note that the Seq of VariableRuntimeData is added as a member to the
    VariableMap since in some cases (e.g. debugging, setting external
    variables), we do need a way to lookup the VRD by QName. This will be
    relatively slow, so any performance critical sections should find the
    VRD during compilation and use that during runtime.
    
    This also removes the VariableMapFactory, it doesn't add much value
    except a level of indirection.
    
    Also removes the allExternalVariables val. Nothing uses it and
    VariableMap handles checking external variables itself by inspecting the
    VariableRuntimeData. We don't need this and it is trivial enough to
    implement if something else needs it.
    
    In a large schema with small files and lots of suspensions, this saw
    around a 50% improvement in unparse performance.
    
    DAFFODIL-2852
---
 .../org/apache/daffodil/core/dsom/SchemaSet.scala  |   9 +-
 .../core/runtime1/SchemaSetRuntime1Mixin.scala     |   8 +-
 .../core/runtime1/VariableMapFactory.scala         |  30 --
 .../core/runtime1/VariableRuntime1Mixin.scala      |   2 +
 .../org/apache/daffodil/core/util/TestUtils.scala  |   3 +-
 .../daffodil/runtime1/processors/RuntimeData.scala |   1 +
 .../runtime1/processors/VariableMap1.scala         | 313 ++++++++++-----------
 .../daffodil/section07/variables/variables.tdml    |  25 ++
 .../section07/variables/TestVariables.scala        |   2 +
 9 files changed, 183 insertions(+), 210 deletions(-)

diff --git 
a/daffodil-core/src/main/scala/org/apache/daffodil/core/dsom/SchemaSet.scala 
b/daffodil-core/src/main/scala/org/apache/daffodil/core/dsom/SchemaSet.scala
index 7512dab21..6a76ae2f6 100644
--- a/daffodil-core/src/main/scala/org/apache/daffodil/core/dsom/SchemaSet.scala
+++ b/daffodil-core/src/main/scala/org/apache/daffodil/core/dsom/SchemaSet.scala
@@ -571,12 +571,9 @@ final class SchemaSet private (
     Seq(encDFV, boDFV, binDFV, outDFV)
   }
 
-  lazy val allDefinedVariables = schemas.flatMap {
-    _.defineVariables
-  }
-  lazy val allExternalVariables = allDefinedVariables.filter {
-    _.external
-  }
+  lazy val allDefinedVariables = schemas
+    .flatMap(_.defineVariables)
+    .union(predefinedVars)
 
   private lazy val checkUnusedProperties = LV('hasUnusedProperties) {
     root.checkUnusedProperties
diff --git 
a/daffodil-core/src/main/scala/org/apache/daffodil/core/runtime1/SchemaSetRuntime1Mixin.scala
 
b/daffodil-core/src/main/scala/org/apache/daffodil/core/runtime1/SchemaSetRuntime1Mixin.scala
index 32eb6461d..7e26c0a27 100644
--- 
a/daffodil-core/src/main/scala/org/apache/daffodil/core/runtime1/SchemaSetRuntime1Mixin.scala
+++ 
b/daffodil-core/src/main/scala/org/apache/daffodil/core/runtime1/SchemaSetRuntime1Mixin.scala
@@ -18,7 +18,6 @@
 package org.apache.daffodil.core.runtime1
 
 import org.apache.daffodil.core.dsom.SchemaSet
-import org.apache.daffodil.core.grammar.VariableMapFactory
 import org.apache.daffodil.lib.util.Logger
 import org.apache.daffodil.runtime1.api.DFDL
 import org.apache.daffodil.runtime1.processors.DataProcessor
@@ -36,11 +35,8 @@ trait SchemaSetRuntime1Mixin {
   requiredEvaluationsAlways(root.elementRuntimeData.initialize)
 
   override lazy val variableMap: VariableMap = LV('variableMap) {
-    val dvs = allSchemaDocuments.flatMap {
-      _.defineVariables
-    }
-    val alldvs = dvs.union(predefinedVars)
-    val vmap = VariableMapFactory.create(alldvs)
+    val vrds = allDefinedVariables.map { _.variableRuntimeData }
+    val vmap = VariableMap(vrds)
     vmap
   }.value
 
diff --git 
a/daffodil-core/src/main/scala/org/apache/daffodil/core/runtime1/VariableMapFactory.scala
 
b/daffodil-core/src/main/scala/org/apache/daffodil/core/runtime1/VariableMapFactory.scala
deleted file mode 100644
index 7ac3cfa42..000000000
--- 
a/daffodil-core/src/main/scala/org/apache/daffodil/core/runtime1/VariableMapFactory.scala
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.daffodil.core.grammar
-
-import org.apache.daffodil.core.dsom.DFDLDefineVariable
-import org.apache.daffodil.runtime1.processors.VariableMap
-
-object VariableMapFactory {
-
-  def create(dvs: Seq[DFDLDefineVariable]): VariableMap = {
-    val vrds = dvs.map { _.variableRuntimeData }
-    val vmap = new VariableMap(vrds)
-    vmap
-  }
-}
diff --git 
a/daffodil-core/src/main/scala/org/apache/daffodil/core/runtime1/VariableRuntime1Mixin.scala
 
b/daffodil-core/src/main/scala/org/apache/daffodil/core/runtime1/VariableRuntime1Mixin.scala
index 80ad9c6e1..6b0250f36 100644
--- 
a/daffodil-core/src/main/scala/org/apache/daffodil/core/runtime1/VariableRuntime1Mixin.scala
+++ 
b/daffodil-core/src/main/scala/org/apache/daffodil/core/runtime1/VariableRuntime1Mixin.scala
@@ -46,6 +46,7 @@ trait DFDLDefineVariableRuntime1Mixin { self: 
DFDLDefineVariable =>
       this.namedQName.asInstanceOf[GlobalQName],
       this.primType,
       this.tunable.unqualifiedPathStepPolicy,
+      this.schemaSet.allDefinedVariables.indexOf(this),
     )
     vrd
   }
@@ -97,6 +98,7 @@ trait DFDLNewVariableInstanceRuntime1Mixin { self: 
DFDLNewVariableInstance =>
       defv.namedQName.asInstanceOf[GlobalQName],
       defv.primType,
       this.tunable.unqualifiedPathStepPolicy,
+      this.schemaSet.allDefinedVariables.indexOf(defv),
     )
     vrd
   }
diff --git 
a/daffodil-core/src/test/scala/org/apache/daffodil/core/util/TestUtils.scala 
b/daffodil-core/src/test/scala/org/apache/daffodil/core/util/TestUtils.scala
index 812a240a0..b009e10b9 100644
--- a/daffodil-core/src/test/scala/org/apache/daffodil/core/util/TestUtils.scala
+++ b/daffodil-core/src/test/scala/org/apache/daffodil/core/util/TestUtils.scala
@@ -30,7 +30,6 @@ import scala.xml._
 
 import org.apache.daffodil.core.compiler.Compiler
 import org.apache.daffodil.core.dsom._
-import org.apache.daffodil.core.grammar.VariableMapFactory
 import org.apache.daffodil.io.InputSourceDataInputStream
 import org.apache.daffodil.lib.Implicits._
 import org.apache.daffodil.lib.api._
@@ -389,7 +388,7 @@ class Fakes private () {
     override def isError: Boolean = false
 
     override def tunables: DaffodilTunables = DaffodilTunables()
-    override def variableMap: VariableMap = VariableMapFactory.create(Nil)
+    override def variableMap: VariableMap = VariableMap(Nil)
     override def validationMode: ValidationMode.Type = ValidationMode.Full
 
     override def withExternalVariables(extVars: Seq[Binding]): 
DFDL.DataProcessor = this
diff --git 
a/daffodil-runtime1/src/main/scala/org/apache/daffodil/runtime1/processors/RuntimeData.scala
 
b/daffodil-runtime1/src/main/scala/org/apache/daffodil/runtime1/processors/RuntimeData.scala
index b00caeb65..5424638ed 100644
--- 
a/daffodil-runtime1/src/main/scala/org/apache/daffodil/runtime1/processors/RuntimeData.scala
+++ 
b/daffodil-runtime1/src/main/scala/org/apache/daffodil/runtime1/processors/RuntimeData.scala
@@ -980,6 +980,7 @@ final class VariableRuntimeData(
   val globalQName: GlobalQName,
   val primType: NodeInfo.PrimType,
   unqualifiedPathStepPolicyArg: UnqualifiedPathStepPolicy,
+  val vmapIndex: Int,
 ) extends NonTermRuntimeData(
     null, // no variable map
     schemaFileLocationArg,
diff --git 
a/daffodil-runtime1/src/main/scala/org/apache/daffodil/runtime1/processors/VariableMap1.scala
 
b/daffodil-runtime1/src/main/scala/org/apache/daffodil/runtime1/processors/VariableMap1.scala
index f00949393..4ae4cbbac 100644
--- 
a/daffodil-runtime1/src/main/scala/org/apache/daffodil/runtime1/processors/VariableMap1.scala
+++ 
b/daffodil-runtime1/src/main/scala/org/apache/daffodil/runtime1/processors/VariableMap1.scala
@@ -17,9 +17,6 @@
 
 package org.apache.daffodil.runtime1.processors
 
-import scala.collection.mutable.ArrayBuffer
-import scala.collection.mutable.Map
-
 import org.apache.daffodil.lib.api.ThinDiagnostic
 import org.apache.daffodil.lib.api.WarnID
 import org.apache.daffodil.lib.exceptions.Assert
@@ -218,7 +215,17 @@ final class VariableBox(initialVMap: VariableMap) {
     vmap_ = newMap
   }
 
-  def cloneForSuspension(): VariableBox = new 
VariableBox(vmap.topLevelInstances)
+  def cloneForSuspension(): VariableBox = new 
VariableBox(vmap.cloneForSuspension)
+}
+
+object VariableMap {
+  def apply(vrds: Seq[VariableRuntimeData] = Nil) = {
+    val table = new Array[Seq[VariableInstance]](vrds.size)
+    vrds.foreach { vrd =>
+      table(vrd.vmapIndex) = Seq(vrd.createVariableInstance())
+    }
+    new VariableMap(vrds, table)
+  }
 }
 
 /**
@@ -233,21 +240,26 @@ final class VariableBox(initialVMap: VariableMap) {
  * The DPath implementation must be made to implement the
  * no-set-after-default-value-has-been-read behavior. This requires that 
reading the variables causes a state transition.
  *
- * The implementation of the VariabeMap uses ArrayBuffers essentially as a
- * stack, as they allow for easy serialization unlike the custom MStack classes
- * we use elsewhere. Scala's mutable Stack is deprecated in 2.12
+ * The VariableMap implementation essentially uses Seq as a stack, as they 
allow for easy
+ * serialization unlike the custom MStack classes we use elsewhere. Note that 
Scala's mutable
+ * Stack is deprecated in 2.12. Additionally, this stack is rarely changed 
(only when
+ * newVariableInstance occurs) so limiting allocations is not critical. 
Further, suspensions
+ * create a copy of the vTable, which is a relatively expensive operation if 
using something
+ * else like an ArrayBuffer or MStack, since Seq's are immutable and copies 
are virtually free.
+ *
+ * Note that each index in the vTable array contains the VariableInstance 
stack for a specific
+ * variable, with that index defined by the "vmapIndex" member of the variables
+ * VariableRuntimeData. This implementation allows for constant lookups by 
indexing into the
+ * vTable array and accessing the head of the VariableInstance sequence. 
Additionally, in some
+ * cases (like with suspensions) we have to make a copy of the array. This is 
the primary reason
+ * for using an Array as opposed to another data structure that allows 
constant lookups like a
+ * Map--array copies are quite fast, and we know the exact size since all 
variables must be
+ * defined at compile time. Although this adds extra complexity compared to a 
Map since we must
+ * carry around an index, the performance gains are worth it.
  */
-class VariableMap private (vTable: Map[GlobalQName, 
ArrayBuffer[VariableInstance]])
+class VariableMap private (vrds: Seq[VariableRuntimeData], vTable: 
Array[Seq[VariableInstance]])
   extends Serializable {
 
-  def this(topLevelVRDs: Seq[VariableRuntimeData] = Nil) =
-    this(Map(topLevelVRDs.map { vrd =>
-      val variab = vrd.createVariableInstance()
-      val variableInstances = new ArrayBuffer[VariableInstance]
-      variableInstances += variab
-      (vrd.globalQName, variableInstances)
-    }: _*))
-
   override def toString(): String = {
     "VariableMap(" + vTable.mkString(" | ") + ")"
   }
@@ -257,26 +269,18 @@ class VariableMap private (vTable: Map[GlobalQName, 
ArrayBuffer[VariableInstance
    * VariableInstances are mutable and cannot safely be shared across threads
    */
   def copy(): VariableMap = {
-    val table = vTable.map {
-      case (k: GlobalQName, variableInstances: ArrayBuffer[VariableInstance]) 
=> {
-        val newBuf = variableInstances.map { _.copy() }
-        (k, newBuf)
-      }
+    val table = vTable.map { variableInstances =>
+      val newBuf = variableInstances.map { _.copy() }
+      newBuf
     }
 
-    new VariableMap(table)
+    new VariableMap(vrds, table)
   }
 
-  def topLevelInstances(): VariableMap = {
-    val table = vTable.map {
-      case (k: GlobalQName, variableInstances: ArrayBuffer[VariableInstance]) 
=> {
-        val newBuf = new ArrayBuffer[VariableInstance]()
-        newBuf.append(variableInstances.last)
-        (k, newBuf)
-      }
-    }
-
-    new VariableMap(table)
+  def cloneForSuspension(): VariableMap = {
+    val newTable = new Array[Seq[VariableInstance]](vTable.size)
+    Array.copy(vTable, 0, newTable, 0, vTable.size)
+    new VariableMap(vrds, newTable)
   }
 
   // For defineVariable's with non-constant expressions for default values, it
@@ -285,20 +289,11 @@ class VariableMap private (vTable: Map[GlobalQName, 
ArrayBuffer[VariableInstance
   // must also ensure that the expressions only reference other variables that
   // have default value expressions or are defined externally.
   def forceExpressionEvaluations(state: ParseOrUnparseState): Unit = {
-    vTable.foreach {
-      case (_, variableInstances) => {
-        variableInstances.foreach { inst =>
-          {
-            (inst.state, inst.rd.maybeDefaultValueExpr.isDefined) match {
-              // Evaluate defineVariable statements with non-constant default 
value expressions
-              case (VariableUndefined, true) => {
-                val res = inst.rd.maybeDefaultValueExpr.get.evaluate(state)
-                inst.setDefaultValue(DataValue.unsafeFromAnyRef(res))
-              }
-              case (_, _) => // Do nothing
-            }
-          }
-        }
+    vTable.foreach { variableInstances =>
+      val inst = variableInstances.head
+      if ((inst.state eq VariableUndefined) && 
inst.rd.maybeDefaultValueExpr.isDefined) {
+        val res = inst.rd.maybeDefaultValueExpr.get.evaluate(state)
+        inst.setDefaultValue(DataValue.unsafeFromAnyRef(res))
       }
     }
   }
@@ -311,31 +306,33 @@ class VariableMap private (vTable: Map[GlobalQName, 
ArrayBuffer[VariableInstance
    * expression
    */
   def setFirstInstanceInitialValues(): Unit = {
-    vTable.foreach {
-      case (_, variableInstances) => {
-        variableInstances(0).firstInstanceInitialValue = 
variableInstances(0).value
-      }
+    vTable.foreach { variableInstances =>
+      variableInstances.head.firstInstanceInitialValue = 
variableInstances.head.value
     }
   }
 
+  /**
+   * Performance of this is linear in number of variables, this should not be 
used in
+   * performance critical sections.
+   */
   def find(qName: GlobalQName): Option[VariableInstance] = {
-    val optBuf = vTable.get(qName)
-    val variab = {
-      if (optBuf.isDefined)
-        Some(optBuf.get.last)
-      else
-        None
-    }
-    variab
+    getVariableRuntimeData(qName).map { vrd => vTable(vrd.vmapIndex).head }
   }
 
-  def qnames(): Seq[GlobalQName] = {
-    vTable.toSeq.map(_._1)
+  /**
+   * Performance of this is linear in number of variables, this should not be 
used in
+   * performance critical sections.
+   */
+  lazy val qnames: Seq[GlobalQName] = {
+    vrds.map { _.globalQName }
   }
 
+  /**
+   * Performance of this is linear in number of variables, this should not be 
used in
+   * performance critical sections.
+   */
   def getVariableRuntimeData(qName: GlobalQName): Option[VariableRuntimeData] 
= {
-    val optVariable = find(qName)
-    if (optVariable.isDefined) Some(optVariable.get.rd) else None
+    vrds.find { _.globalQName == qName }
   }
 
   lazy val context = Assert.invariantFailed("unused.")
@@ -348,14 +345,9 @@ class VariableMap private (vTable: Map[GlobalQName, 
ArrayBuffer[VariableInstance
    * perform
    */
   def readVariableWillChangeState(vrd: VariableRuntimeData): Boolean = {
-    val variableInstances = vTable.get(vrd.globalQName)
-    if (variableInstances.isDefined) {
-      val variable = variableInstances.get.last
-      val variableRead = (variable.state eq VariableRead) && 
variable.value.isDefined
-      !variableRead
-    } else {
-      true
-    }
+    val variable = vTable(vrd.vmapIndex).head
+    val variableRead = (variable.state eq VariableRead) && 
variable.value.isDefined
+    !variableRead
   }
 
   /**
@@ -384,41 +376,34 @@ class VariableMap private (vTable: Map[GlobalQName, 
ArrayBuffer[VariableInstance
       case _ => // Do nothing
     }
 
-    val variableInstances = vTable.get(varQName)
-    if (variableInstances.isDefined) {
-      val variable = variableInstances.get.last
-      variable.state match {
-        case VariableRead if (variable.value.isDefined) => 
variable.value.getNonNullable
-        case VariableDefined | VariableSet if (variable.value.isDefined) => {
-          variable.setState(VariableRead)
-          variable.value.getNonNullable
-        }
-        // This case is only hit for defineVariable's who's expression 
reference
-        // other defineVariables with expressions. It will be hit at the start
-        // of parsing, before an infoset is generated, via the
-        // forceExpressionEvaluations function after which all variables should
-        // have a defined value
-        case VariableUndefined if 
(variable.rd.maybeDefaultValueExpr.isDefined) => {
-          variable.setState(VariableBeingDefined)
-          val res =
-            
DataValue.unsafeFromAnyRef(variable.rd.maybeDefaultValueExpr.get.evaluate(state))
-
-          // Need to update the variable's value with the result of the
-          // expression
-          variable.setState(VariableRead)
-          variable.setValue(res)
-
-          res
-        }
-        case VariableBeingDefined => throw new 
VariableCircularDefinition(varQName, vrd)
-        case VariableInProcess => throw new VariableSuspended(varQName, vrd)
-        case _ => throw new VariableHasNoValue(varQName, vrd)
+    val variable = vTable(vrd.vmapIndex).head
+    variable.state match {
+      case VariableRead if (variable.value.isDefined) => 
variable.value.getNonNullable
+      case VariableDefined | VariableSet if (variable.value.isDefined) => {
+        variable.setState(VariableRead)
+        variable.value.getNonNullable
+      }
+      // This case is only hit for defineVariable's who's expression reference
+      // other defineVariables with expressions. It will be hit at the start
+      // of parsing, before an infoset is generated, via the
+      // forceExpressionEvaluations function after which all variables should
+      // have a defined value
+      case VariableUndefined if (variable.rd.maybeDefaultValueExpr.isDefined) 
=> {
+        variable.setState(VariableBeingDefined)
+        val res =
+          
DataValue.unsafeFromAnyRef(variable.rd.maybeDefaultValueExpr.get.evaluate(state))
+
+        // Need to update the variable's value with the result of the
+        // expression
+        variable.setState(VariableRead)
+        variable.setValue(res)
+
+        res
       }
-    } else
-      referringContext.SDE(
-        "Variable map (compilation): unknown variable %s",
-        varQName,
-      ) // Fix DFDL-766
+      case VariableBeingDefined => throw new 
VariableCircularDefinition(varQName, vrd)
+      case VariableInProcess => throw new VariableSuspended(varQName, vrd)
+      case _ => throw new VariableHasNoValue(varQName, vrd)
+    }
   }
 
   /**
@@ -431,59 +416,57 @@ class VariableMap private (vTable: Map[GlobalQName, 
ArrayBuffer[VariableInstance
     pstate: ParseOrUnparseState,
   ) = {
     val varQName = vrd.globalQName
-    val variableInstances = vTable.get(varQName)
-    if (variableInstances.isDefined) {
-      val variable = variableInstances.get.last
-      variable.state match {
-        case VariableSet => {
-          referringContext.SDE(
-            "Cannot set variable %s twice. State was: %s. Existing value: %s",
-            variable.rd.globalQName,
-            VariableSet,
-            variable.value,
-          )
-        }
+    val variableInstances = vTable(vrd.vmapIndex)
+    val variable = variableInstances.head
+    variable.state match {
+      case VariableSet => {
+        referringContext.SDE(
+          "Cannot set variable %s twice. State was: %s. Existing value: %s",
+          variable.rd.globalQName,
+          VariableSet,
+          variable.value,
+        )
+      }
 
-        case VariableRead => {
+      case VariableRead => {
+
+        /**
+         * TODO: This should be an SDE, but due to a bug (DAFFODIL-1443) in
+         * the way we evaluate escapeSchemes it could lead us to setting the
+         * variable read too early */
+        pstate.SDW(
+          WarnID.VariableSet,
+          "Cannot set variable %s after reading the default value. State was: 
%s. Existing value: %s",
+          variable.rd.globalQName,
+          VariableSet,
+          variable.value,
+        )
+        variable.setValue(newValue)
+        variable.setState(VariableSet)
+      }
 
+      case _ => {
+        vrd.direction match {
           /**
-           * TODO: This should be an SDE, but due to a bug (DAFFODIL-1443) in
-           * the way we evaluate escapeSchemes it could lead us to setting the
-           * variable read too early */
-          pstate.SDW(
-            WarnID.VariableSet,
-            "Cannot set variable %s after reading the default value. State 
was: %s. Existing value: %s",
-            variable.rd.globalQName,
-            VariableSet,
-            variable.value,
-          )
-          variable.setValue(newValue)
-          variable.setState(VariableSet)
-        }
-
-        case _ => {
-          vrd.direction match {
-            /**
-             * Due to potential race conditions regarding the setting of
-             * variables via setVariable and default values in combination with
-             * suspensions during unparsing, we only allow the use of either
-             * setVariable statements or a default value when unparsing a
-             * variable.
-             */
-            case VariableDirection.UnparseOnly | VariableDirection.Both
-                if (vrd.maybeDefaultValueExpr.isDefined && 
variableInstances.get.size > 1) => {
-              // Variable has an unparse direction, a default value, and a
-              // newVariableInstance
-              pstate.SDE(
-                "Variable %s has an unparse direction and a default value, 
setting the variable may cause race conditions when combined with a forward 
referencing expression.",
-                varQName,
-              )
-            }
-            case _ => // Do nothing
+           * Due to potential race conditions regarding the setting of
+           * variables via setVariable and default values in combination with
+           * suspensions during unparsing, we only allow the use of either
+           * setVariable statements or a default value when unparsing a
+           * variable.
+           */
+          case VariableDirection.UnparseOnly | VariableDirection.Both
+              if (vrd.maybeDefaultValueExpr.isDefined && 
variableInstances.size > 1) => {
+            // Variable has an unparse direction, a default value, and a
+            // newVariableInstance
+            pstate.SDE(
+              "Variable %s has an unparse direction and a default value, 
setting the variable may cause race conditions when combined with a forward 
referencing expression.",
+              varQName,
+            )
           }
-          variable.setValue(newValue)
-          variable.setState(VariableSet)
+          case _ => // Do nothing
         }
+        variable.setValue(newValue)
+        variable.setState(VariableSet)
       }
     }
   }
@@ -492,20 +475,17 @@ class VariableMap private (vTable: Map[GlobalQName, 
ArrayBuffer[VariableInstance
    * Creates a new instance of a variable without default value
    */
   def newVariableInstance(vrd: VariableRuntimeData): VariableInstance = {
-    val varQName = vrd.globalQName
-    val variableInstances = vTable.get(varQName)
-    Assert.invariant(variableInstances.isDefined)
+    val variableInstances = vTable(vrd.vmapIndex)
     val nvi = vrd.createVariableInstance()
-    nvi.firstInstanceInitialValue = 
variableInstances.get(0).firstInstanceInitialValue
-    variableInstances.get += nvi
+    nvi.firstInstanceInitialValue = 
variableInstances.head.firstInstanceInitialValue
+    vTable(vrd.vmapIndex) = nvi +: variableInstances
     nvi
   }
 
   def removeVariableInstance(vrd: VariableRuntimeData): Unit = {
-    val varQName = vrd.globalQName
-    val variableInstances = vTable.get(varQName)
-    Assert.invariant(variableInstances.isDefined && 
variableInstances.get.nonEmpty)
-    variableInstances.get.trimEnd(1)
+    val variableInstances = vTable(vrd.vmapIndex)
+    Assert.invariant(variableInstances.nonEmpty)
+    vTable(vrd.vmapIndex) = variableInstances.tail
   }
 
   /**
@@ -524,21 +504,22 @@ class VariableMap private (vTable: Map[GlobalQName, 
ArrayBuffer[VariableInstance
         // solving this ambiguity if only one of those varibles was external,
         // but it's safer to err on the side of caution and require the user to
         // be explicit about which variable they intended to set.
-        val candidates = vTable.filter { case (key, _) =>
-          key.local == bindingQName.local
+        val candidates = vrds.filter { vrd =>
+          vrd.globalQName.local == bindingQName.local
         }
         candidates.size match {
           case 0 => None
-          case 1 => Some(candidates.head._2)
+          case 1 => Some(vTable(candidates.head.vmapIndex))
           case _ => {
             val msg =
               "External variable binding %s is ambiguous. A namespace is 
required to resolve the ambiguity. Found variables: %s"
-                .format(bindingQName, 
candidates.keys.map(_.toString).mkString(", "))
+                .format(bindingQName, 
candidates.map(_.globalQName.toString).mkString(", "))
             throw new ExternalVariableException(msg)
           }
         }
       } else {
-        vTable.get(bindingQName.toGlobalQName)
+        val optVrd = vrds.find { _.globalQName == bindingQName.toGlobalQName }
+        optVrd.map { vrd => vTable(vrd.vmapIndex) }
       }
 
     optVariableInstances match {
diff --git 
a/daffodil-test/src/test/resources/org/apache/daffodil/section07/variables/variables.tdml
 
b/daffodil-test/src/test/resources/org/apache/daffodil/section07/variables/variables.tdml
index 88b784f51..290b5540a 100644
--- 
a/daffodil-test/src/test/resources/org/apache/daffodil/section07/variables/variables.tdml
+++ 
b/daffodil-test/src/test/resources/org/apache/daffodil/section07/variables/variables.tdml
@@ -2754,5 +2754,30 @@
     </tdml:infoset>
   </tdml:parserTestCase>
 
+  <tdml:defineSchema name="backwardsDefines">
+    <!--
+      Daffodil evaluates variable defaults in the order the variables are
+      defined. This schema ensures that variable defaults that reference later
+      defined variables still works, i.e. defineVariable order does not matter
+    -->
+    <dfdl:defineVariable name="three" type="xs:int" defaultValue="{ $two + 1 
}" />
+    <dfdl:defineVariable name="two" type="xs:int" defaultValue="{ $one + 1 }" 
/>
+    <dfdl:defineVariable name="one" type="xs:int" defaultValue="1" />
+
+    <xs:include 
schemaLocation="/org/apache/daffodil/xsd/DFDLGeneralFormat.dfdl.xsd"/>
+    <dfdl:format ref="ex:GeneralFormat" />
+
+    <xs:element name="three" type="xs:int" dfdl:inputValueCalc="{ $three }" />
+
+  </tdml:defineSchema>
+
+  <tdml:parserTestCase name="backwardsDefines_01" root="three" 
model="backwardsDefines">
+    <tdml:document></tdml:document>
+    <tdml:infoset>
+      <tdml:dfdlInfoset>
+        <three>3</three>
+      </tdml:dfdlInfoset>
+    </tdml:infoset>
+  </tdml:parserTestCase>
 
 </tdml:testSuite>
diff --git 
a/daffodil-test/src/test/scala/org/apache/daffodil/section07/variables/TestVariables.scala
 
b/daffodil-test/src/test/scala/org/apache/daffodil/section07/variables/TestVariables.scala
index d87c0ebfc..166e0aa48 100644
--- 
a/daffodil-test/src/test/scala/org/apache/daffodil/section07/variables/TestVariables.scala
+++ 
b/daffodil-test/src/test/scala/org/apache/daffodil/section07/variables/TestVariables.scala
@@ -165,6 +165,8 @@ class TestVariables {
 
   @Test def test_nviInGroupDecl_01(): Unit = { 
runner.runOneTest("nviInGroupDecl_01") }
 
+  @Test def test_backwardsDefines_01: Unit = { 
runner.runOneTest("backwardsDefines_01") }
+
   /*****************************************************************/
   val tdmlVal = XMLUtils.TDML_NAMESPACE
   val dfdl = XMLUtils.DFDL_NAMESPACE

Reply via email to