stevedlawrence commented on a change in pull request #450:
URL: https://github.com/apache/incubator-daffodil/pull/450#discussion_r539280896



##########
File path: daffodil-lib/src/main/scala/org/apache/daffodil/util/MStack.scala
##########
@@ -182,7 +182,7 @@ object MStackOfAnyRef {
  * things.
  */
 protected abstract class MStack[@specialized T] private[util] (
-  arrayAllocator: (Int) => Array[T], nullValue: T) extends Serializable {
+  arrayAllocator: (Int) => Array[T], nullValue: T) {

Review comment:
       Is this intentional? Is there a problem with serializing an MStack?

##########
File path: 
daffodil-runtime1-unparser/src/main/scala/org/apache/daffodil/processors/unparsers/ExpressionEvaluatingUnparsers.scala
##########
@@ -87,7 +87,8 @@ class NewVariableInstanceStartUnparser(override val context: 
RuntimeData)
   override lazy val childProcessors = Vector()
 
   override def unparse(state: UState) = {
-    state.newVariableInstance(context.asInstanceOf[VariableRuntimeData])
+    val vrd = context.asInstanceOf[VariableRuntimeData]
+    state.newVariableInstance(vrd, vrd, state)

Review comment:
       It looks like in all cases where we call newVariableInstance, we pass in 
the same thing as the first and second paramter, and the same state as the 
third parameter. Can newVariableInstance just only accept one parameter and 
always use vrd when it needs VariableRuntimeData, and always use ``this`` when 
it needs state?

##########
File path: 
daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/DataProcessor.scala
##########
@@ -435,6 +435,11 @@ class DataProcessor private (
       state.notifyDebugging(true)
     }
     state.dataProc.get.init(ssrd.parser)
+
+    // Force the evaluation of any defineVariable's with non-constant default
+    // value expressions
+    state.variableMap.forceExpressionEvaluations(state)
+

Review comment:
       You mention there's an issue with the TDML runner not seeing this 
diagnostic 
([DAFFODIL-2443](https://issues.apache.org/jira/browse/DAFFODIL-2443)). I think 
this change might be the issue.
   
   When you force the expression evaluate for defaults, that could fail and 
throw an SDE exception. There's nothing here to catch it, so it exception will 
bubble up to the TDML Runner. But the TDML Runner does (and shouldn't) handle 
SDE exceptions. Instead SDE exceptions are supposed to be caught by Daffodil 
and turned into a failure diagnostic. That catch happens in the doParse method 
(along with a bunch of other exceptions that might be thrown that implied some 
sort of error that should become a diagnostic).
   
   So I think the fix is to just move this forceExpressionEvaluations call into 
the try-block of the doParse method. That way if this function does throw an 
SDE, then it will be caught and correctly turned into a diagnostic. I think it 
can be moved to right before the startElement call and be funcionally 
equivalent to what you have now, just with the SDE handled correctly.

##########
File path: 
daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/VariableMap1.scala
##########
@@ -188,24 +283,42 @@ class VariableMap private(vTable: Map[GlobalQName, 
MStackOf[VariableInstance]])
    * VariableInstances are mutable and cannot safely be shared across threads
    */
   def copy(): VariableMap = {
-    val table = Map[GlobalQName, MStackOf[VariableInstance]]()
-    vTable.foreach { case (k: GlobalQName, s: MStackOf[VariableInstance]) => {
-      val newStack= new MStackOf[VariableInstance]()
+    val table = Map[GlobalQName, ArrayBuffer[VariableInstance]]()
+    vTable.foreach { case (k: GlobalQName, abuf: 
ArrayBuffer[VariableInstance]) => {
+      val newBuf = new ArrayBuffer[VariableInstance]()
 
       // toList provides a list in LIFO order, so we need to reverse it to
       // maintain order
-      s.toList.reverse.foreach { case v: VariableInstance => 
newStack.push(v.copy()) }
-      table(k) = newStack
+      abuf.foreach { case v: VariableInstance => { newBuf += v.copy() }}

Review comment:
       The comment above about toList seems to no longer apply. I think it can 
be removed. Also, I think the foreach's can be replaced with maps. Should make 
things ab it cleaner, e.g.:
   ```scala
   val table = vTable.map { case (k: GlobalQName, abuf: 
ArrayBuffer[VariableInstance]) =>
     val newBuf = abuf.map { _.copy() }
     (k, newBuf)
   }
   new VariableMap(table)
   ```

##########
File path: 
daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/VariableMap1.scala
##########
@@ -188,24 +283,42 @@ class VariableMap private(vTable: Map[GlobalQName, 
MStackOf[VariableInstance]])
    * VariableInstances are mutable and cannot safely be shared across threads
    */
   def copy(): VariableMap = {
-    val table = Map[GlobalQName, MStackOf[VariableInstance]]()
-    vTable.foreach { case (k: GlobalQName, s: MStackOf[VariableInstance]) => {
-      val newStack= new MStackOf[VariableInstance]()
+    val table = Map[GlobalQName, ArrayBuffer[VariableInstance]]()
+    vTable.foreach { case (k: GlobalQName, abuf: 
ArrayBuffer[VariableInstance]) => {
+      val newBuf = new ArrayBuffer[VariableInstance]()
 
       // toList provides a list in LIFO order, so we need to reverse it to
       // maintain order
-      s.toList.reverse.foreach { case v: VariableInstance => 
newStack.push(v.copy()) }
-      table(k) = newStack
+      abuf.foreach { case v: VariableInstance => { newBuf += v.copy() }}
+      table(k) = newBuf
     }}
 
     new VariableMap(table)
   }
 
+  // For defineVariable's with non-constant expressions for default values, it
+  // is necessary to force the evaluation of the expressions after the
+  // VariableMap has been created and initialized, but before parsing begins. 
We
+  // 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.map { case (_, abuf) => { abuf.foreach { inst => {

Review comment:
       This map can be a foreach. No need to allocate a new Map since we're 
mutate the abuf's inside it.

##########
File path: 
daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/VariableMap1.scala
##########
@@ -232,19 +345,33 @@ class VariableMap private(vTable: Map[GlobalQName, 
MStackOf[VariableInstance]])
    */
   def readVariable(vrd: VariableRuntimeData, referringContext: ThrowsSDE, 
maybePstate: Maybe[ParseOrUnparseState]): DataValuePrimitive = {
     val varQName = vrd.globalQName
-    val stack = vTable.get(varQName)
-    if (stack.isDefined) {
-      val variable = stack.get.top
-      variable.state match {
-        case VariableRead if (variable.value.isDefined) => 
variable.value.getNonNullable
-        case VariableDefined | VariableSet if (variable.value.isDefined) => {
+    val abuf = vTable.get(varQName)
+    if (abuf.isDefined) {
+      val variable = abuf.get.last
+      (variable.state, variable.value.isDefined, 
variable.defaultValueExpr.isDefined) match {
+        case (VariableRead, true, _) => variable.value.getNonNullable
+        case (VariableDefined | VariableSet, true, _) => {
           if (maybePstate.isDefined && maybePstate.get.isInstanceOf[PState])
             maybePstate.get.asInstanceOf[PState].markVariableRead(vrd)
 
           variable.setState(VariableRead)
           variable.value.getNonNullable
         }
-        case _ => throw new VariableHasNoValue(varQName, vrd)
+        case (VariableDefined, false, true) => {
+          Assert.invariant(maybePstate.isDefined)
+          variable.setState(VariableInProcess)
+          val pstate = maybePstate.get
+          val res = 
DataValue.unsafeFromAnyRef(variable.defaultValueExpr.get.evaluate(pstate))
+
+          // Need to update the variable's value with the result of the
+          // expression
+          variable.setState(VariableRead)
+          variable.setValue(res)
+
+          res

Review comment:
       Is this correct? This is evaluating the defaultValue and setting the 
state when the variable is read. But the spec says this about 
newVariableInstance:
   
   > If a default value is specified this initial value of the instance will be 
created when the instance is created
   
   So it seems this evaluation should not happen during read, but when the 
instance of the variable is created. And then that default could be overridden 
with a setVariableCal.
   
   Looks like you do evaluate the default expression below in 
newVariableInstance, so maybe this just needs to use what that sets?

##########
File path: 
daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/VariableMap1.scala
##########
@@ -286,23 +413,28 @@ class VariableMap private(vTable: Map[GlobalQName, 
MStackOf[VariableInstance]])
   /**
    * Creates a new instance of a variable
    */
-  def newVariableInstance(vrd: VariableRuntimeData) = {
+  def newVariableInstance(vrd: VariableRuntimeData, referringContext: 
ThrowsSDE, state: ParseOrUnparseState) = {
     val varQName = vrd.globalQName
-    val stack = vTable.get(varQName)
-    Assert.invariant(stack.isDefined)
-    stack.get.push(vrd.createVariableInstance)
+    val abuf = vTable.get(varQName)
+    Assert.invariant(abuf.isDefined)
+
+    if (vrd.maybeDefaultValueExpr.isDefined) {
+      val defaultValue = 
DataValue.unsafeFromAnyRef(vrd.maybeDefaultValueExpr.get.evaluate(state))
+      abuf.get += 
vrd.createVariableInstance(VariableUtils.convert(defaultValue.getAnyRef.toString,
 vrd, referringContext))

Review comment:
       This ``VariableUtils.convert`` seems incorrect to me. I would expect 
that evaluating the defaultValue expression, or any setVariable expression as 
well, would know the target type and the expression evaluation would handle the 
conversion, just like it does with all other expression evaluations. Looks like 
this isn't something you added, so probably out of scope for this change. But 
we should create a bug to fix this.

##########
File path: 
daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/VariableMap1.scala
##########
@@ -232,19 +345,33 @@ class VariableMap private(vTable: Map[GlobalQName, 
MStackOf[VariableInstance]])
    */
   def readVariable(vrd: VariableRuntimeData, referringContext: ThrowsSDE, 
maybePstate: Maybe[ParseOrUnparseState]): DataValuePrimitive = {
     val varQName = vrd.globalQName
-    val stack = vTable.get(varQName)
-    if (stack.isDefined) {
-      val variable = stack.get.top
-      variable.state match {
-        case VariableRead if (variable.value.isDefined) => 
variable.value.getNonNullable
-        case VariableDefined | VariableSet if (variable.value.isDefined) => {
+    val abuf = vTable.get(varQName)
+    if (abuf.isDefined) {
+      val variable = abuf.get.last
+      (variable.state, variable.value.isDefined, 
variable.defaultValueExpr.isDefined) match {
+        case (VariableRead, true, _) => variable.value.getNonNullable
+        case (VariableDefined | VariableSet, true, _) => {
           if (maybePstate.isDefined && maybePstate.get.isInstanceOf[PState])
             maybePstate.get.asInstanceOf[PState].markVariableRead(vrd)
 
           variable.setState(VariableRead)
           variable.value.getNonNullable
         }
-        case _ => throw new VariableHasNoValue(varQName, vrd)
+        case (VariableDefined, false, true) => {
+          Assert.invariant(maybePstate.isDefined)
+          variable.setState(VariableInProcess)
+          val pstate = maybePstate.get
+          val res = 
DataValue.unsafeFromAnyRef(variable.defaultValueExpr.get.evaluate(pstate))
+
+          // Need to update the variable's value with the result of the
+          // expression
+          variable.setState(VariableRead)
+          variable.setValue(res)
+
+          res
+        }
+        case (VariableInProcess, _, _) => throw new 
VariableCircularDefinition(varQName, vrd)
+        case (_, _, _) => throw new VariableHasNoValue(varQName, vrd)

Review comment:
       Should the test we have add coverage for this, but it's currently 
failing due to DAFFODIL-2444? This seems like a really important path to cover.

##########
File path: 
daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/VariableMap1.scala
##########
@@ -166,17 +257,21 @@ 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 implementaiton of the VariabeMap uses ArrayBuffers essentially as a

Review comment:
       Typo: implementation

##########
File path: 
daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/VariableMap1.scala
##########
@@ -232,19 +345,33 @@ class VariableMap private(vTable: Map[GlobalQName, 
MStackOf[VariableInstance]])
    */
   def readVariable(vrd: VariableRuntimeData, referringContext: ThrowsSDE, 
maybePstate: Maybe[ParseOrUnparseState]): DataValuePrimitive = {
     val varQName = vrd.globalQName
-    val stack = vTable.get(varQName)
-    if (stack.isDefined) {
-      val variable = stack.get.top
-      variable.state match {
-        case VariableRead if (variable.value.isDefined) => 
variable.value.getNonNullable
-        case VariableDefined | VariableSet if (variable.value.isDefined) => {
+    val abuf = vTable.get(varQName)
+    if (abuf.isDefined) {
+      val variable = abuf.get.last
+      (variable.state, variable.value.isDefined, 
variable.defaultValueExpr.isDefined) match {
+        case (VariableRead, true, _) => variable.value.getNonNullable
+        case (VariableDefined | VariableSet, true, _) => {
           if (maybePstate.isDefined && maybePstate.get.isInstanceOf[PState])

Review comment:
       While we're renaming variables, can we change maybePstate to just 
maybeState, since it seems it can be either parse or unparse? I thought this 
was specifically for parse, and was confused about what happens during unparse, 
but that doesn't seem to be the case. Or should this always be PState, and 
maybe the type of maybePState should be Maybe[PState] in which case you only 
need th isDefined check?

##########
File path: 
daffodil-test/src/test/resources/org/apache/daffodil/section07/variables/variables.tdml
##########
@@ -1884,4 +1976,63 @@
     </tdml:infoset>
   </tdml:parserTestCase>
 
+  <tdml:defineSchema name="circular_defineVariable">
+    <xs:include 
schemaLocation="org/apache/daffodil/xsd/DFDLGeneralFormat.dfdl.xsd"/>
+    <dfdl:format ref="ex:GeneralFormat" />
+
+    <dfdl:defineVariable name="circular1" type="xs:int"
+      defaultValue="{ $ex:circular2 }" />
+    <dfdl:defineVariable name="circular2" type="xs:int"
+      defaultValue="{ $ex:circular1 }" />
+    <xs:element name="root" type="xs:int" dfdl:inputValueCalc="{ $ex:circular1 
}" />
+
+  </tdml:defineSchema>
+
+  <tdml:parserTestCase name="circular_defineVariable_err" root="root"
+    model="circular_defineVariable" description="Section 7 - ">
+    <tdml:document/>
+    <tdml:errors>
+      <tdml:error>Runtime Schema Definition Error</tdml:error>
+      <tdml:error>part of circular definition</tdml:error>
+    </tdml:errors>
+  </tdml:parserTestCase>
+
+  <tdml:defineSchema name="defineVariable_ref_infoset">
+    <xs:include 
schemaLocation="org/apache/daffodil/xsd/DFDLGeneralFormat.dfdl.xsd"/>
+    <dfdl:format ref="ex:GeneralFormat" />
+
+    <dfdl:defineVariable name="badRef" type="xs:int"
+      defaultValue="{ ex:root }" />
+    <xs:element name="root" type="xs:int" dfdl:inputValueCalc="{ 42 }" />
+
+  </tdml:defineSchema>
+
+  <tdml:parserTestCase name="defineVariable_ref_infoset_err" root="root"
+    model="defineVariable_ref_infoset" description="Section 7 - ">
+    <tdml:document/>
+    <tdml:errors>
+      <tdml:error>????</tdml:error>
+    </tdml:errors>
+  </tdml:parserTestCase>
+
+  <tdml:defineSchema name="defineVariable_ref_noDefault">
+    <xs:include 
schemaLocation="org/apache/daffodil/xsd/DFDLGeneralFormat.dfdl.xsd"/>
+    <dfdl:format ref="ex:GeneralFormat" />
+
+    <dfdl:defineVariable name="noDefault" type="xs:int" />
+    <dfdl:defineVariable name="badRef" type="xs:int"
+      defaultValue="{ $ex:noDefault }" />
+    <xs:element name="root" type="xs:int" dfdl:inputValueCalc="{ $ex:badRef }" 
/>
+
+  </tdml:defineSchema>
+
+  <tdml:parserTestCase name="defineVariable_ref_noDefault_err" root="root"
+    model="defineVariable_ref_noDefault" description="Section 7 - ">
+    <tdml:document/>
+    <tdml:errors>
+      <tdml:error>Runtime Schema Definition Error</tdml:error>
+      <tdml:error>has no value. It was not set</tdml:error>
+    </tdml:errors>
+  </tdml:parserTestCase>
+

Review comment:
       An intersting test case that I'm not sure is covered but would be worth 
having: What if you have two variables where they both have defaults, and one 
refers to the other, like what you have above:
   ```xml
       <dfdl:defineVariable name="var1" type="xs:int" defaultValue="5" />
       <dfdl:defineVariable name="var2" type="xs:int" defaultValue="{ $var1 + 
1 }" />
   ```
   Now what if during parse we have a dfdl:setVariable that sets the value of 
both of these variables? The spec says this:
   > If a defaultValue expression references another variable then the 
single-assignment nature of variables prevents the referenced variable's value 
from ever changing, that is, it is considered to be a read of the variable's 
value, and once read, a variable's value cannot be changed.
   
   So we shoudl be able to setVariable var2, but not setVariable var1. I think 
having a test that checks for this would be useful.
   
   Another test I think would be useful, if it doesn't already exist, would be 
to allow var1 and var2 to be externally set, and make sure that those values 
override the default values. With the change with the forceEvaluations and 
delay evaluates of things, I'm not 100% confident that works as expected.

##########
File path: 
daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/VariableMap1.scala
##########
@@ -232,19 +345,33 @@ class VariableMap private(vTable: Map[GlobalQName, 
MStackOf[VariableInstance]])
    */
   def readVariable(vrd: VariableRuntimeData, referringContext: ThrowsSDE, 
maybePstate: Maybe[ParseOrUnparseState]): DataValuePrimitive = {
     val varQName = vrd.globalQName
-    val stack = vTable.get(varQName)
-    if (stack.isDefined) {
-      val variable = stack.get.top
-      variable.state match {
-        case VariableRead if (variable.value.isDefined) => 
variable.value.getNonNullable
-        case VariableDefined | VariableSet if (variable.value.isDefined) => {
+    val abuf = vTable.get(varQName)
+    if (abuf.isDefined) {
+      val variable = abuf.get.last
+      (variable.state, variable.value.isDefined, 
variable.defaultValueExpr.isDefined) match {
+        case (VariableRead, true, _) => variable.value.getNonNullable
+        case (VariableDefined | VariableSet, true, _) => {
           if (maybePstate.isDefined && maybePstate.get.isInstanceOf[PState])
             maybePstate.get.asInstanceOf[PState].markVariableRead(vrd)
 
           variable.setState(VariableRead)
           variable.value.getNonNullable
         }
-        case _ => throw new VariableHasNoValue(varQName, vrd)
+        case (VariableDefined, false, true) => {
+          Assert.invariant(maybePstate.isDefined)
+          variable.setState(VariableInProcess)
+          val pstate = maybePstate.get
+          val res = 
DataValue.unsafeFromAnyRef(variable.defaultValueExpr.get.evaluate(pstate))

Review comment:
       I guess this means maybePState really could be a UState or a PState?

##########
File path: 
daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/VariableMap1.scala
##########
@@ -232,19 +345,33 @@ class VariableMap private(vTable: Map[GlobalQName, 
MStackOf[VariableInstance]])
    */
   def readVariable(vrd: VariableRuntimeData, referringContext: ThrowsSDE, 
maybePstate: Maybe[ParseOrUnparseState]): DataValuePrimitive = {
     val varQName = vrd.globalQName
-    val stack = vTable.get(varQName)
-    if (stack.isDefined) {
-      val variable = stack.get.top
-      variable.state match {
-        case VariableRead if (variable.value.isDefined) => 
variable.value.getNonNullable
-        case VariableDefined | VariableSet if (variable.value.isDefined) => {
+    val abuf = vTable.get(varQName)
+    if (abuf.isDefined) {
+      val variable = abuf.get.last
+      (variable.state, variable.value.isDefined, 
variable.defaultValueExpr.isDefined) match {

Review comment:
       This is maybe personal preference, but I prefer the previous code where 
we had ``case Foo if ...``. It's easier to see what a particular case is 
checking for when you can see the condition, rather than just seeing true/false 
and having to refer back to the match statement. There also might be some 
overhead with having to allocate a tuple (not sure if Scala will do that behind 
the scenes).




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to