stevedlawrence commented on code in PR #1199:
URL: https://github.com/apache/daffodil/pull/1199#discussion_r1544395957


##########
daffodil-core/src/main/scala/org/apache/daffodil/core/dpath/Expression.scala:
##########
@@ -3041,6 +3047,101 @@ case class DFDLCheckConstraintsExpr(
 
 }
 
+case class DFDLCheckRangeInclusiveExpr(
+  nameAsParsed: String,
+  fnQName: RefQName,
+  args: List[Expression],
+) extends FunctionCallBase(nameAsParsed, fnQName, args) {
+
+  lazy val List(arg1, arg2, arg3) = { checkArgCount(3); args }
+
+  override lazy val children = args
+
+  override lazy val compiledDPath = {
+    checkArgCount(3)
+    val argDPath = args(0).compiledDPath
+    val rangeFrom = args(1).compiledDPath
+    val rangeTo = args(2).compiledDPath
+    val c = conversions
+    val res = new CompiledDPath(DFDLCheckRangeInclusive(argDPath, rangeFrom, 
rangeTo) +: c)
+    res
+  }
+  override def targetTypeForSubexpression(subexpr: Expression): NodeInfo.Kind 
= {
+    val targetType = (arg1.inherentType, arg2.inherentType, arg3.inherentType) 
match {
+      case (
+            testType: NodeInfo.Numeric.Kind,
+            minType: NodeInfo.Numeric.Kind,
+            maxType: NodeInfo.Numeric.Kind,
+          ) => {
+        val rangeType = NodeInfoUtils.generalizeArgTypesForComparisonOp(
+          "checkRangeInclusive",
+          minType,
+          maxType,
+        )
+        val targetType = NodeInfoUtils.generalizeArgTypesForComparisonOp(
+          "checkRangeInclusive",
+          testType,
+          rangeType,
+        )
+        targetType
+      }
+      case (test, min, max) =>
+        SDE(s"Cannot call $nameAsParsed with non-numeric types: $test, $min, 
$max")
+    }
+    targetType
+  }
+
+  override lazy val inherentType: NodeInfo.Kind = NodeInfo.Boolean
+
+}
+
+case class DFDLCheckRangeExclusiveExpr(
+  nameAsParsed: String,
+  fnQName: RefQName,
+  args: List[Expression],
+) extends FunctionCallBase(nameAsParsed, fnQName, args) {
+
+  lazy val List(arg1, arg2, arg3) = { checkArgCount(3); args }
+
+  override lazy val children = args
+
+  override lazy val compiledDPath = {
+    checkArgCount(3)
+    val argDPath = args(0).compiledDPath
+    val rangeFrom = args(1).compiledDPath
+    val rangeTo = args(2).compiledDPath
+    val c = conversions
+    val res = new CompiledDPath(DFDLCheckRangeExclusive(argDPath, rangeFrom, 
rangeTo) +: c)

Review Comment:
   DFDLCheckRangeExclusiveExpr is exactly the same as the Inclusive version 
except for this one line. What are your thoughts on only having a single 
`DFDLCheckRangeExpr` that takes a boolean parameter (e.g. `isExcsluve`) that 
signifies inclusive vs exclusive? Then, using a suggestion below you could have 
a single `DFDLCheckRange` class that accepts min/max comparision operations, 
something like:
   
   ```scala
   val (_, _, lt, le, _) = ComparisonOpts.forType(targetType)
   val compare = if (isExclusive) lt else le
   val res = new CompiledDPath(DFDLCheckRange(argDPath, rangeFrom, rangeTo, 
compare) +: c)
   ```
   Then we can combine DFDLCheckRangeInclusive/Exclusive to a single 
`DFDLCheckRange` class that does something like:
   ```scala
   def run(...) {
     ...
     val res = compare.operate(minValue, dataValue) && 
compare.operate(dataValue, maxValue)
     res
   }
   ```
   
   The benefit to this is approach is we avoid lots of duplication and we also 
figure out the right comparison logic/function at schema compile time instead 
of run-time.



##########
daffodil-runtime1/src/main/scala/org/apache/daffodil/runtime1/dpath/DFDLFunctions.scala:
##########
@@ -39,6 +42,94 @@ case class DFDLCheckConstraints(recipe: CompiledDPath) 
extends RecipeOpWithSubRe
   }
 }
 
+case class DFDLCheckRangeInclusive(
+  dataRecipe: CompiledDPath,
+  minRecipe: CompiledDPath,
+  maxRecipe: CompiledDPath,
+) extends RecipeOpWithSubRecipes(dataRecipe, minRecipe, maxRecipe) {
+  override def run(dstate: DState): Unit = {
+    val saved = dstate.currentNode
+    dataRecipe.run(dstate)
+    val dataVal = dstate.currentValue
+    dstate.setCurrentNode(saved)
+    minRecipe.run(dstate)
+    val minVal = dstate.currentValue
+    dstate.setCurrentNode(saved)
+    maxRecipe.run(dstate)
+    val maxVal = dstate.currentValue
+
+    val res = executeCheck(
+      dataVal: DataValue.DataValuePrimitiveNullable,
+      minVal: DataValue.DataValuePrimitiveNullable,
+      maxVal: DataValue.DataValuePrimitiveNullable,
+    )
+    dstate.setCurrentValue(res)
+  }
+
+  def executeCheck(
+    dataVal: DataValue.DataValuePrimitiveNullable,
+    minVal: DataValue.DataValuePrimitiveNullable,
+    maxVal: DataValue.DataValuePrimitiveNullable,
+  ): Boolean = {
+    (dataVal.value, minVal.value, maxVal.value) match {
+      case (data: Integer, min: Integer, max: Integer) => data >= min && data 
<= max
+      case (data: java.lang.Double, min: java.lang.Double, max: 
java.lang.Double) =>
+        data >= min && data <= max
+      case (data: java.lang.Float, min: java.lang.Float, max: java.lang.Float) 
=>
+        data >= min && data <= max
+      case (data: java.math.BigDecimal, min: java.math.BigDecimal, max: 
java.math.BigDecimal) =>
+        data.compareTo(min) >= 0 && data.compareTo(max) <= 0
+      case (data: BigInteger, min: BigInteger, max: BigInteger) =>
+        data.compareTo(min) >= 0 && data.compareTo(max) <= 0

Review Comment:
   This match case isn't exhaustive. It's missing Short, Byte, and Long.
   
   Though, instead of doing the type check at runtime and duplicating a bunch 
of checks, what if we figure out the right 
[ComparisonOps](https://github.com/apache/daffodil/blob/main/daffodil-runtime1/src/main/scala/org/apache/daffodil/runtime1/dpath/ComparisonOps.scala)
 to use at compile time?
   
   I suggest we create a new static function, something like
   
   ```scala
   object ComparisonOps {
     def forType(type: NodeInfo.Kind): (CompareOpBase, CompareOpBase, 
CompareOpBase, CompareOpBase, CompareOpBase, CompareOpBase) = {
       type match {
         case PrimType.Boolean => (EQ_Compare, NE_Compare, LT_Boolean, 
LE_Boolean, GT_Boolean, GE_Boolean)
         case PrimType.Integer => (EQ_Compare, NE_Compare, LT_Integer, 
LE_Integer, GT_Integer, GE_Integer)
         ...
       }
     }
     ...
   }
   ```
   So it returns a 6-tuple of all the comparison functions that might be needed 
for a specific type. We can use this anywhere we need comparison functions, 
like in the `DFDLCheckRangeExpr` suggested above. We could also use it in 
`ComparisonExpression`. That probably wants to be something like this:
   
   ```
   lazy val compareOp = {
     val (eq, ne, lt, le, gt, ge) = CopmarisonOps.forType(convertedArgType)
     op match {
       ...
       case "eq"=> eq
       case "ne" => ne
       case "lt" => lt
       ...
     }
   }
   ```
   Note that all the comparison functions used in `ComparisionExpression` 
should be used in the new `forType` function.
   
   We could also could use it in `repTypeComparers`, e.g.
   
   ```scala
   lazy val (
     repTypeCompareLT: NumberCompareOp,
     repTypeCompareLE: NumberCompareOp,
   ) = LV('repTypeComparers) {
     val (_, _, lt, le, _, _) = 
ComparisonOps.forType(repTypeElementDec.primType)
     (lt, le)
   }
   ```
   
   There might be other places where we have a match/case to figure out 
comparison ops that could be simplified in this way an remove some duplication.



##########
daffodil-core/src/main/scala/org/apache/daffodil/core/dpath/Expression.scala:
##########
@@ -3041,6 +3047,101 @@ case class DFDLCheckConstraintsExpr(
 
 }
 
+case class DFDLCheckRangeInclusiveExpr(
+  nameAsParsed: String,
+  fnQName: RefQName,
+  args: List[Expression],
+) extends FunctionCallBase(nameAsParsed, fnQName, args) {
+
+  lazy val List(arg1, arg2, arg3) = { checkArgCount(3); args }
+
+  override lazy val children = args
+
+  override lazy val compiledDPath = {
+    checkArgCount(3)
+    val argDPath = args(0).compiledDPath
+    val rangeFrom = args(1).compiledDPath
+    val rangeTo = args(2).compiledDPath

Review Comment:
   Also, if you reference arg1/arg2/arg3 it will force evaluation of line 3056 
and call checkArgCount(3), so you can also remove the duplicate check on 3061.



##########
daffodil-runtime1/src/main/scala/org/apache/daffodil/runtime1/dpath/DFDLFunctions.scala:
##########
@@ -39,6 +42,94 @@ case class DFDLCheckConstraints(recipe: CompiledDPath) 
extends RecipeOpWithSubRe
   }
 }
 
+case class DFDLCheckRangeInclusive(
+  dataRecipe: CompiledDPath,
+  minRecipe: CompiledDPath,
+  maxRecipe: CompiledDPath,
+) extends RecipeOpWithSubRecipes(dataRecipe, minRecipe, maxRecipe) {
+  override def run(dstate: DState): Unit = {
+    val saved = dstate.currentNode
+    dataRecipe.run(dstate)
+    val dataVal = dstate.currentValue
+    dstate.setCurrentNode(saved)
+    minRecipe.run(dstate)
+    val minVal = dstate.currentValue
+    dstate.setCurrentNode(saved)
+    maxRecipe.run(dstate)
+    val maxVal = dstate.currentValue
+
+    val res = executeCheck(
+      dataVal: DataValue.DataValuePrimitiveNullable,
+      minVal: DataValue.DataValuePrimitiveNullable,
+      maxVal: DataValue.DataValuePrimitiveNullable,
+    )
+    dstate.setCurrentValue(res)
+  }
+
+  def executeCheck(
+    dataVal: DataValue.DataValuePrimitiveNullable,
+    minVal: DataValue.DataValuePrimitiveNullable,
+    maxVal: DataValue.DataValuePrimitiveNullable,
+  ): Boolean = {
+    (dataVal.value, minVal.value, maxVal.value) match {
+      case (data: Integer, min: Integer, max: Integer) => data >= min && data 
<= max
+      case (data: java.lang.Double, min: java.lang.Double, max: 
java.lang.Double) =>
+        data >= min && data <= max
+      case (data: java.lang.Float, min: java.lang.Float, max: java.lang.Float) 
=>
+        data >= min && data <= max
+      case (data: java.math.BigDecimal, min: java.math.BigDecimal, max: 
java.math.BigDecimal) =>
+        data.compareTo(min) >= 0 && data.compareTo(max) <= 0
+      case (data: BigInteger, min: BigInteger, max: BigInteger) =>
+        data.compareTo(min) >= 0 && data.compareTo(max) <= 0
+      case (_, _, _) => false
+    }
+  }
+}
+
+case class DFDLCheckRangeExclusive(
+  dataRecipe: CompiledDPath,
+  minRecipe: CompiledDPath,
+  maxRecipe: CompiledDPath,
+) extends RecipeOpWithSubRecipes(dataRecipe, minRecipe, maxRecipe) {
+  override def run(dstate: DState): Unit = {
+    val saved = dstate.currentNode
+    dataRecipe.run(dstate)
+    val dataVal = dstate.currentValue
+    dstate.setCurrentNode(saved)
+    minRecipe.run(dstate)
+    val minVal = dstate.currentValue
+    dstate.setCurrentNode(saved)
+    maxRecipe.run(dstate)
+    val maxVal = dstate.currentValue

Review Comment:
   The `currentNode` DState can be used by a `run` implementation to know where 
to evaluate a relative path expression from (e.g. `../foo/bar/baz`).
   
   But calling `run` can also potentially mutate `currentNode`, changing it to 
something else, which can interfere with other calls to run, so it needs to be 
reset.
   
   For example, say we had something like this:
   
   ```
   dfdl:checkRangeExclusive(../foo/bar/baz, ../uno/dos/tres, ../one/two/three)`
   ```
   
   Before we call `run()`, let's say `currentNode` points to an element called 
`root`.
   
   After we call `dataRecipe.run()` which evaluates `../foo/bar/baz` relative 
to `root`, the `currentNode` will point the `baz` element, because DState was 
mutated. If we do not reset `currentNode` back to `root`, then when we call 
`minRecipe.run()` to evaluate `../uno/dos/tres` that relative path will be 
evaluated relative to the `baz` element instead of the `root` element. So 
that's why we need to save and restore curentNode after each call to run.
   
   Note that we don't have to restore `currentNode` after calling 
`maxReipce.run()` because we aren't personally calling anymore runs, so 
whatever `currentNode` ended up as doesn't matter to us. If some other 
expression was called that led to this run() being called, then they were 
responsible for saving and restoring currentNode if they needed it.



-- 
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.

To unsubscribe, e-mail: [email protected]

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

Reply via email to