You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@daffodil.apache.org by ji...@apache.org on 2023/06/09 15:05:59 UTC

[daffodil] branch main updated: Add codegen-c support for runtime length hexBinary

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

jinterrante 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 47f385ef4 Add codegen-c support for runtime length hexBinary
47f385ef4 is described below

commit 47f385ef4fee938662c54619d08595824de587fb
Author: John Interrante <in...@research.ge.com>
AuthorDate: Thu Jun 8 11:00:45 2023 -0400

    Add codegen-c support for runtime length hexBinary
    
    Extend Daffodil's C code generator to support runtime length hexBinary
    elements.  Also make some cleanups for better consistency.
    
    DAFFODIL-2819
    
    dependency-scan.yml: Saw a better example somewhere (it was called
    dependency-graph.yml) so modify and rename to match that example.
    
    DaffodilCCodeGenerator.scala: Change comments and names in compileCode
    and pickCommand to be more similar to another codegen.  Extend
    generateCode to handle RepOrderedWithMinMaxSequenceChild.
    
    BinaryValueCodeGenerator.scala: Remove restriction on runtime lengths.
    
    CodeGeneratorState.scala: Add cExpression to implement a poor man's C
    code expression generator until Daffodil adds custom code generation
    functionality to compiled DFDL expressions.  Remove namespace prefixes
    in makeLegalForC to ensure they don't appear in C code.  Extend
    arrayMaxOccurs to handle more cases.  Make cStructFieldAccess do a
    better job of converting a DFDL expression written as a path to a C
    struct field dereference.
    
    HexBinaryCodeGenerator.scala: Allow specified length hexBinary
    elements to use runtime lengths as well as constant lengths.
    
    ex_nums.tdml: Add a new test to check support for runtime length
    hexBinary elements.
    
    Evaluatable.scala: Allow codegen-c to get compiled expression from
    EvaluatableExpression.
    
    TestExNums.scala: Make both daffodil and daffodilC run new test.
---
 .../{dependency-scan.yml => dependency-graph.yml}  | 27 +++----
 .../codegen/c/DaffodilCCodeGenerator.scala         | 50 ++++++++-----
 .../c/generators/BinaryValueCodeGenerator.scala    |  5 --
 .../codegen/c/generators/CodeGeneratorState.scala  | 87 ++++++++++++++++++----
 .../c/generators/HexBinaryCodeGenerator.scala      | 47 ++++++++++--
 .../org/apache/daffodil/codegen/c/ex_nums.tdml     | 42 ++++++++++-
 .../daffodil/runtime1/processors/Evaluatable.scala |  2 +-
 .../org/apache/daffodil/codegen/c/TestExNums.scala |  2 +
 8 files changed, 196 insertions(+), 66 deletions(-)

diff --git a/.github/workflows/dependency-scan.yml b/.github/workflows/dependency-graph.yml
similarity index 50%
rename from .github/workflows/dependency-scan.yml
rename to .github/workflows/dependency-graph.yml
index 3a1fd3252..2308d5b65 100644
--- a/.github/workflows/dependency-scan.yml
+++ b/.github/workflows/dependency-graph.yml
@@ -13,30 +13,23 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-name: Dependency scan
+name: Dependency Graph
+
+# Run after pushing a commit to the main branch.
 
-# Controls when the workflow will run
 on:
-  # Triggers the workflow on push or pull request events but only for the "main" branch
   push:
-    branches: [ "main" ]
-    
-  # Allows you to run this workflow manually from the Actions tab
-  workflow_dispatch:
+    branches:
+      - main
 
-# A workflow run is made up of one or more jobs that can run sequentially or in parallel
 jobs:
-  # This workflow contains a single job called "build"
-  build:
-    # The type of runner that the job will run on
+  dependency-graph:
+    name: Dependency Graph
     runs-on: ubuntu-22.04
 
-    # Steps represent a sequence of tasks that will be executed as part of the job
     steps:
-      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
-      - uses: actions/checkout@v3.5.2
+      - name: Check out Repository
+        uses: actions/checkout@v3.5.2
 
-      - name: Sbt Dependency Submission
-        # You may pin to the exact commit or the version.
-        # uses: scalacenter/sbt-dependency-submission@ab086b50c947c9774b70f39fc7f6e20ca2706c91
+      - name: Submit Dependency Graph
         uses: scalacenter/sbt-dependency-submission@v2.1.2
diff --git a/daffodil-codegen-c/src/main/scala/org/apache/daffodil/codegen/c/DaffodilCCodeGenerator.scala b/daffodil-codegen-c/src/main/scala/org/apache/daffodil/codegen/c/DaffodilCCodeGenerator.scala
index 3f438fc5f..fc92e1134 100644
--- a/daffodil-codegen-c/src/main/scala/org/apache/daffodil/codegen/c/DaffodilCCodeGenerator.scala
+++ b/daffodil-codegen-c/src/main/scala/org/apache/daffodil/codegen/c/DaffodilCCodeGenerator.scala
@@ -52,6 +52,7 @@ import org.apache.daffodil.core.grammar.primitives.HexBinarySpecifiedLength
 import org.apache.daffodil.core.grammar.primitives.OrderedSequence
 import org.apache.daffodil.core.grammar.primitives.RepOrderedExactlyNSequenceChild
 import org.apache.daffodil.core.grammar.primitives.RepOrderedExpressionOccursCountSequenceChild
+import org.apache.daffodil.core.grammar.primitives.RepOrderedWithMinMaxSequenceChild
 import org.apache.daffodil.core.grammar.primitives.RightFill
 import org.apache.daffodil.core.grammar.primitives.ScalarOrderedSequenceChild
 import org.apache.daffodil.core.grammar.primitives.SpecifiedLengthExplicit
@@ -143,21 +144,21 @@ class DaffodilCCodeGenerator(root: Root) extends DFDL.CodeGenerator {
     val exe = if (isWin) codeDir / "daffodil.exe" else codeDir / "daffodil"
 
     try {
-      // Assemble the compiler's command line arguments
-      val compiler = pickCompiler
+      // Assemble the compilation command line arguments
+      val command = pickCommand
       val cFlags = Seq("-std=gnu11")
       val includes = Seq("-Ilibcli", "-Ilibruntime")
       val absFiles = os.walk(codeDir, skip = _.last == "tests").filter(_.ext == "c")
       val relFiles = Seq("libcli/*.c", "libruntime/*.c")
       val libs = Seq("-lmxml")
 
-      // Run the compiler within the code directory
-      if (compiler.nonEmpty) {
+      // Run the compilation command within the code directory
+      if (command.nonEmpty) {
         val result = os
-          .proc(compiler, cFlags, includes, if (isWin) relFiles else absFiles, libs, "-o", exe)
+          .proc(command, cFlags, includes, if (isWin) relFiles else absFiles, libs, "-o", exe)
           .call(cwd = codeDir, stderr = os.Pipe)
         if (result.chunks.nonEmpty) {
-          // Report any compiler output as a warning
+          // Report any compilation output as a warning
           warning(result.toString())
         }
       }
@@ -179,26 +180,27 @@ class DaffodilCCodeGenerator(root: Root) extends DFDL.CodeGenerator {
   }
 
   /**
-   * Searches for any available C compiler on the system.  Tries to find the
-   * compiler given by `CC` if `CC` exists in the environment, then tries to
-   * find any compiler from the following list:
+   * Searches for any available C compilation command on the system.
+   * Tries to find the command given by `CC` if `CC` exists in the
+   * environment, then tries to find any command from the following
+   * list:
    *
    *   - zig cc
    *   - cc
    *   - clang
    *   - gcc
    *
-   * Returns the first compiler found as a sequence of strings in case the
-   * compiler is a program with a subcommand argument.  Returns the empty
-   * sequence if no compiler could be found in the user's PATH.
+   * Returns the first command found as a sequence of strings in case
+   * the command has a subcommand argument.  Returns the empty
+   * sequence if no command could be found in the user's PATH.
    */
-  private lazy val pickCompiler: Seq[String] = {
+  private lazy val pickCommand: Seq[String] = {
     val ccEnv = sys.env.getOrElse("CC", "zig cc")
-    val compilers = Seq(ccEnv, "zig cc", "cc", "clang", "gcc")
+    val commands = Seq(ccEnv, "zig cc", "cc", "clang", "gcc")
     val path = sys.env.getOrElse("PATH", ".").split(File.pathSeparatorChar)
-    def inPath(compiler: String): Boolean = {
-      (compiler != null) && {
-        val exec = compiler.takeWhile(_ != ' ')
+    def inPath(command: String): Boolean = {
+      (command != null) && {
+        val exec = command.takeWhile(_ != ' ')
         val exec2 = exec + ".exe"
         path.exists(dir =>
           Files.isExecutable(Paths.get(dir, exec))
@@ -206,9 +208,9 @@ class DaffodilCCodeGenerator(root: Root) extends DFDL.CodeGenerator {
         )
       }
     }
-    val compiler = compilers.find(inPath)
-    if (compiler.isDefined)
-      compiler.get.split(' ').toSeq
+    val command = commands.find(inPath)
+    if (command.isDefined)
+      command.get.split(' ').toSeq
     else
       Seq.empty[String]
   }
@@ -278,6 +280,7 @@ object DaffodilCCodeGenerator
       case g: RepOrderedExactlyNSequenceChild => repOrderedExactlyNSequenceChild(g, cgState)
       case g: RepOrderedExpressionOccursCountSequenceChild =>
         repOrderedExpressionOccursCountSequenceChild(g, cgState)
+      case g: RepOrderedWithMinMaxSequenceChild => repOrderedWithMinMaxSequenceChild(g, cgState)
       case g: RightFill => noop(g)
       case g: ScalarOrderedSequenceChild => scalarOrderedSequenceChild(g, cgState)
       case g: SeqComp => seqCompGenerateCode(g, cgState)
@@ -343,6 +346,13 @@ object DaffodilCCodeGenerator
     cgState.popArray(g.context)
   }
 
+  private def repOrderedWithMinMaxSequenceChild(
+    g: RepOrderedWithMinMaxSequenceChild,
+    cgState: CodeGeneratorState,
+  ): Unit = {
+    DaffodilCCodeGenerator.generateCode(g.term.termContentBody, cgState)
+  }
+
   private def scalarOrderedSequenceChild(
     g: ScalarOrderedSequenceChild,
     cgState: CodeGeneratorState,
diff --git a/daffodil-codegen-c/src/main/scala/org/apache/daffodil/codegen/c/generators/BinaryValueCodeGenerator.scala b/daffodil-codegen-c/src/main/scala/org/apache/daffodil/codegen/c/generators/BinaryValueCodeGenerator.scala
index 374e08e5d..3d6bd871a 100644
--- a/daffodil-codegen-c/src/main/scala/org/apache/daffodil/codegen/c/generators/BinaryValueCodeGenerator.scala
+++ b/daffodil-codegen-c/src/main/scala/org/apache/daffodil/codegen/c/generators/BinaryValueCodeGenerator.scala
@@ -20,7 +20,6 @@ package org.apache.daffodil.codegen.c.generators
 import org.apache.daffodil.core.dsom.ElementBase
 import org.apache.daffodil.lib.schema.annotation.props.gen.BitOrder
 import org.apache.daffodil.lib.schema.annotation.props.gen.ByteOrder
-import org.apache.daffodil.lib.schema.annotation.props.gen.LengthKind
 import org.apache.daffodil.lib.util.Maybe.Nope
 
 // Base trait which provides common code to generate C code for primitive value elements
@@ -44,10 +43,6 @@ trait BinaryValueCodeGenerator {
       e.maybeByteOrderEv == Nope || e.byteOrderEv.isConstant,
       "Runtime dfdl:byteOrder expressions not supported.",
     )
-    e.schemaDefinitionUnless(
-      e.lengthKind == LengthKind.Prefixed || e.elementLengthInBitsEv.isConstant,
-      "Runtime dfdl:length expressions not supported.",
-    )
 
     // Call the given partially applied function values with their remaining unbound argument (deref)
     val deref = if (cgState.hasArray) "[i]" else ""
diff --git a/daffodil-codegen-c/src/main/scala/org/apache/daffodil/codegen/c/generators/CodeGeneratorState.scala b/daffodil-codegen-c/src/main/scala/org/apache/daffodil/codegen/c/generators/CodeGeneratorState.scala
index 107305a58..ed7e6ec00 100644
--- a/daffodil-codegen-c/src/main/scala/org/apache/daffodil/codegen/c/generators/CodeGeneratorState.scala
+++ b/daffodil-codegen-c/src/main/scala/org/apache/daffodil/codegen/c/generators/CodeGeneratorState.scala
@@ -17,6 +17,7 @@
 
 package org.apache.daffodil.codegen.c.generators
 
+import java.util.regex.Pattern
 import scala.collection.immutable
 import scala.collection.mutable
 
@@ -387,7 +388,7 @@ class CodeGeneratorState(private val root: ElementBase) {
     code.replace("\r\n", "\n").replace("\n", System.lineSeparator)
   }
 
-  // Returns a name for the given element's C struct identifier
+  // Returns a C name for the given element's C struct identifier
   private def cStructName(context: ElementBase): String = {
     val sb = buildName(context, new StringBuilder)
     makeLegalForC(sb)
@@ -395,7 +396,7 @@ class CodeGeneratorState(private val root: ElementBase) {
     name
   }
 
-  // Returns a name for the given element's element runtime data
+  // Returns a C name for the given element's element runtime data
   private def erdName(context: ElementBase): String = {
     val sb = buildName(context, new StringBuilder) ++= "ERD"
     makeLegalForC(sb)
@@ -403,7 +404,7 @@ class CodeGeneratorState(private val root: ElementBase) {
     name
   }
 
-  // Returns a name for the given element's local name
+  // Returns a C name for the given element's local name.
   def cName(context: ElementBase): String = {
     val sb = new StringBuilder(context.namedQName.local)
     makeLegalForC(sb)
@@ -411,6 +412,44 @@ class CodeGeneratorState(private val root: ElementBase) {
     name
   }
 
+  // Converts a dfdl:length expression to a C expression.  We make some
+  // simplifying assumptions to make converting the expression easier:
+  // - all field names start with ../ or /rootName
+  // - all field names end at the first non-XML-identifier character
+  // - all field names can be converted to C identifiers using cStructFieldAccess
+  // - the expression performs only arithmetic (with no casts) and computes a length
+  // - the expression is almost C-ready but may contain some non-C named operators
+  // - may need to replace any div, idiv, mod with / and % instead
+  // Eventually we should make the compiled expression generate the C code itself
+  // instead of using this superficial replaceAllIn approach.
+  def cExpression(expr: String): String = {
+    // Match each field name and replace it with a C struct field dereference
+    val field = """(((\.\./)+|/)[\p{L}_][\p{L}:_\-.0-9/]*)""".r
+    val exprWithFields = field.replaceAllIn(
+      expr,
+      m => {
+        val fieldName = m.group(1).stripPrefix("../")
+        val cFieldName = cStructFieldAccess(fieldName)
+        cFieldName
+      },
+    )
+    // Match each named operator and replace it with a C operator
+    val operator = """(\bidiv\b|\bdiv\b|\bmod\b)""".r
+    val exprWithOperators = operator.replaceAllIn(
+      exprWithFields,
+      m => {
+        val operatorName = m.group(1)
+        val cOperatorSymbol = operatorName match {
+          case "idiv" => "/"
+          case "div" => "/"
+          case "mod" => "%"
+        }
+        cOperatorSymbol
+      },
+    )
+    exprWithOperators
+  }
+
   // Adds an ERD definition for the given complex element
   private def addComplexTypeERD(context: ElementBase): Unit = {
     val C = cStructName(context)
@@ -792,9 +831,14 @@ class CodeGeneratorState(private val root: ElementBase) {
 
   // Makes any XML identifier a legal C identifier
   private def makeLegalForC(sb: StringBuilder): Unit = {
+    // Remove the local name's namespace prefix if there is one; with
+    // luck it won't be needed and the C code will look cleaner
+    val matcher = Pattern.compile("^[^/:]+:").matcher(sb)
+    if (matcher.lookingAt()) sb.replace(matcher.start, matcher.end, "")
+
+    // Replace illegal characters with '_' to form a legal C name
     lazy val legalCharsForC: immutable.Set[Char] =
       Set('_') ++ ('a' to 'z') ++ ('A' to 'Z') ++ ('0' to '9')
-    // Ensure sb contains only legal characters for C identifiers
     for (i <- sb.indices) {
       if (!legalCharsForC.contains(sb.charAt(i))) {
         sb.setCharAt(i, '_')
@@ -880,6 +924,7 @@ class CodeGeneratorState(private val root: ElementBase) {
       case OccursCountKind.Fixed if e.maxOccurs > 1 => e.maxOccurs
       case OccursCountKind.Fixed if e.maxOccurs == 1 => 0
       case OccursCountKind.Implicit if e.minOccurs == 1 && e.maxOccurs == 1 => 0
+      case OccursCountKind.Implicit if e.maxOccurs > 0 => e.maxOccurs
       case OccursCountKind.Expression if e.maxOccurs > 0 => e.maxOccurs
       case _ =>
         e.SDE(
@@ -938,18 +983,31 @@ class CodeGeneratorState(private val root: ElementBase) {
   // - we can convert a relative path without any up dirs to an instance-> indirection
   // - we can convert slashes in the path to dots in a C struct field access notation
   private def cStructFieldAccess(expr: String): String = {
-    // Strip any namespace prefixes from expr and the root element's local name since
-    // C code uses only struct fields' names.
-    val rootName = cName(root)
-    val exprPath = expr.replaceAll("/[^/:]+:", "/").stripPrefix(s"/$rootName")
-    val fieldAccess = if (exprPath.startsWith("/")) {
+    // If expr is an absolute path, strip the root element's local name
+    val rootName = root.namedQName.local
+    val exprWOPrefix = expr.stripPrefix(s"/$rootName")
+
+    // Turn all DFDL local names into legal C names
+    val localName = """([\p{L}_][\p{L}:_\-.0-9]*)""".r
+    val exprWithFields = localName.replaceAllIn(
+      exprWOPrefix,
+      m => {
+        // Make each DFDL local name a legal C name
+        val sb = new StringBuilder(m.group(1))
+        makeLegalForC(sb)
+        sb.mkString
+      },
+    )
+
+    // Convert exprPath to the appropriate field access indirection
+    val fieldAccess = if (exprWithFields.startsWith("/")) {
       // Convert exprPath to a get_infoset()-> indirection
       val C = cStructName(root)
-      s"""(($C *)get_infoset(false))->${exprPath.stripPrefix("/")}"""
-    } else if (exprPath.startsWith("../")) {
+      s"""(($C *)get_infoset(false))->${exprWithFields.stripPrefix("/")}"""
+    } else if (exprWithFields.startsWith("../")) {
       // Split exprPath into the up dirs and after the up dirs
-      val afterUpDirs = exprPath.split("\\.\\./").mkString
-      val upDirs = exprPath.stripSuffix(afterUpDirs)
+      val afterUpDirs = exprWithFields.split("\\.\\./").mkString
+      val upDirs = exprWithFields.stripSuffix(afterUpDirs)
       // Count how many up dirs there are
       val nUpDirs = upDirs.split('/').length
       // Go up the stack that many times to get that struct's C type
@@ -960,8 +1018,9 @@ class CodeGeneratorState(private val root: ElementBase) {
       s"""(($C *)instance->_base.$parents)->$afterUpDirs"""
     } else {
       // Convert exprPath to an instance-> indirection
-      s"""instance->$exprPath"""
+      s"""instance->$exprWithFields"""
     }
+
     // Finally, convert the field access to C struct dot notation
     val notation = fieldAccess.replace('/', '.')
     notation
diff --git a/daffodil-codegen-c/src/main/scala/org/apache/daffodil/codegen/c/generators/HexBinaryCodeGenerator.scala b/daffodil-codegen-c/src/main/scala/org/apache/daffodil/codegen/c/generators/HexBinaryCodeGenerator.scala
index 96d90897a..44d521a6b 100644
--- a/daffodil-codegen-c/src/main/scala/org/apache/daffodil/codegen/c/generators/HexBinaryCodeGenerator.scala
+++ b/daffodil-codegen-c/src/main/scala/org/apache/daffodil/codegen/c/generators/HexBinaryCodeGenerator.scala
@@ -20,6 +20,7 @@ package org.apache.daffodil.codegen.c.generators
 import org.apache.daffodil.core.dsom.ElementBase
 import org.apache.daffodil.lib.schema.annotation.props.gen.ByteOrder
 import org.apache.daffodil.runtime1.dpath.NodeInfo.PrimType
+import org.apache.daffodil.runtime1.processors.ExplicitLengthEv
 
 trait HexBinaryCodeGenerator extends BinaryValueCodeGenerator {
 
@@ -96,20 +97,50 @@ trait HexBinaryCodeGenerator extends BinaryValueCodeGenerator {
     val localName = cgState.cName(e)
     val field = s"instance->$localName$deref"
     val fieldArray = s"instance->_a_$localName$deref"
-    val specifiedLength = e.elementLengthInBitsEv.constValue.get
+    val specifiedLength =
+      if (e.elementLengthInBitsEv.isConstant)
+        e.elementLengthInBitsEv.constValue.get
+      else
+        -1
+    val primType = s"size_t"
+    val lenVar = s"_l_$localName"
+    val expression =
+      if (e.elementLengthInBitsEv.isConstant)
+        e.elementLengthInBitsEv.constValue.get.toString
+      else {
+        val expr = e.elementLengthInBitsEv.lengthEv
+          .asInstanceOf[ExplicitLengthEv]
+          .expr
+          .toBriefXML()
+          .stripPrefix("'{")
+          .stripSuffix("}'")
+          .trim()
+        // Convert DFDL expression to a C expression
+        val cExpr = cgState.cExpression(expr)
+        cExpr
+      }
 
     val initERDStatement =
       if (specifiedLength > 0)
         s"""$indent1$indent2    $field.array = $fieldArray;
-         |$indent1$indent2    $field.lengthInBytes = sizeof($fieldArray);
-         |$indent1$indent2    $field.dynamic = false;""".stripMargin
-      else
+           |$indent1$indent2    $field.lengthInBytes = sizeof($fieldArray);
+           |$indent1$indent2    $field.dynamic = false;""".stripMargin
+      else if (specifiedLength == 0)
         s"""$indent1$indent2    $field.array = NULL;
-         |$indent1$indent2    $field.lengthInBytes = 0;
-         |$indent1$indent2    $field.dynamic = false;""".stripMargin
+           |$indent1$indent2    $field.lengthInBytes = 0;
+           |$indent1$indent2    $field.dynamic = false;""".stripMargin
+      else
+        s"""$indent1$indent2    $field.dynamic = true;""".stripMargin
     val parseStatement =
-      s"""$indent1$indent2    parse_hexBinary(&$field, pstate);
-         |$indent1$indent2    if (pstate->error) return;""".stripMargin
+      if (specifiedLength >= 0)
+        s"""$indent1$indent2    parse_hexBinary(&$field, pstate);
+           |$indent1$indent2    if (pstate->error) return;""".stripMargin
+      else
+        s"""$indent1$indent2    $primType $lenVar = $expression;
+           |$indent1$indent2    alloc_hexBinary(&$field, $lenVar, pstate);
+           |$indent1$indent2    if (pstate->error) return;
+           |$indent1$indent2    parse_hexBinary(&$field, pstate);
+           |$indent1$indent2    if (pstate->error) return;""".stripMargin
     val unparseStatement =
       s"""$indent1$indent2    unparse_hexBinary($field, ustate);
          |$indent1$indent2    if (ustate->error) return;""".stripMargin
diff --git a/daffodil-codegen-c/src/test/resources/org/apache/daffodil/codegen/c/ex_nums.tdml b/daffodil-codegen-c/src/test/resources/org/apache/daffodil/codegen/c/ex_nums.tdml
index 94e81bc08..419d5ef7a 100644
--- a/daffodil-codegen-c/src/test/resources/org/apache/daffodil/codegen/c/ex_nums.tdml
+++ b/daffodil-codegen-c/src/test/resources/org/apache/daffodil/codegen/c/ex_nums.tdml
@@ -21,7 +21,9 @@
   description="TDML tests for ex_nums.dfdl.xsd"
   xmlns:daf="urn:ogf:dfdl:2013:imp:daffodil.apache.org:2018:ext"
   xmlns:dfdl="http://www.ogf.org/dfdl/dfdl-1.0/"
-  xmlns:tdml="http://www.ibm.com/xmlns/dfdl/testData">
+  xmlns:ex="http://example.com"
+  xmlns:tdml="http://www.ibm.com/xmlns/dfdl/testData"
+  xmlns:xs="http://www.w3.org/2001/XMLSchema">
 
   <!--
       Run all tests:
@@ -52,6 +54,44 @@
     </tdml:infoset>
   </tdml:parserTestCase>
 
+  <!--
+      Test runtime length hexBinary works
+  -->
+
+  <tdml:defineSchema name="length">
+    <xs:include schemaLocation="org/apache/daffodil/xsd/DFDLGeneralFormat.dfdl.xsd"/>
+    <dfdl:format ref="GeneralFormat" representation="binary"/>
+    <xs:element name="e1">
+      <xs:complexType>
+        <xs:sequence>
+          <xs:element name="len" type="xs:int"/>
+          <xs:element name="hex" type="xs:hexBinary"
+                      dfdl:length="{ (../ex:len idiv 4) * 4 }"
+                      dfdl:lengthKind="explicit"/>
+        </xs:sequence>
+      </xs:complexType>
+    </xs:element>
+  </tdml:defineSchema>
+
+  <tdml:parserTestCase
+    model="length"
+    name="length"
+    root="e1"
+    roundTrip="onePass">
+    <tdml:document>
+      <tdml:documentPart type="byte">00000004</tdml:documentPart>
+      <tdml:documentPart type="byte">00000004</tdml:documentPart>
+    </tdml:document>
+    <tdml:infoset>
+      <tdml:dfdlInfoset>
+        <e1>
+          <len>4</len>
+          <hex>00000004</hex>
+        </e1>
+      </tdml:dfdlInfoset>
+    </tdml:infoset>
+  </tdml:parserTestCase>
+
   <!--
       In Daffodil, parse with validation="off" or "limited" reads
       non-fixed values with no validation diagnostics, while parse
diff --git a/daffodil-runtime1/src/main/scala/org/apache/daffodil/runtime1/processors/Evaluatable.scala b/daffodil-runtime1/src/main/scala/org/apache/daffodil/runtime1/processors/Evaluatable.scala
index aa65adcc2..435d5bfbd 100644
--- a/daffodil-runtime1/src/main/scala/org/apache/daffodil/runtime1/processors/Evaluatable.scala
+++ b/daffodil-runtime1/src/main/scala/org/apache/daffodil/runtime1/processors/Evaluatable.scala
@@ -451,7 +451,7 @@ trait ExprEvalMixin[T <: AnyRef]
  * of the property.
  */
 abstract class EvaluatableExpression[ExprType <: AnyRef](
-  override protected val expr: CompiledExpression[ExprType],
+  override val expr: CompiledExpression[ExprType],
   ci: DPathCompileInfo,
 ) extends Evaluatable[ExprType](ci)
   with ExprEvalMixin[ExprType] {
diff --git a/daffodil-test/src/test/scala/org/apache/daffodil/codegen/c/TestExNums.scala b/daffodil-test/src/test/scala/org/apache/daffodil/codegen/c/TestExNums.scala
index d1686e9c0..b8e421138 100644
--- a/daffodil-test/src/test/scala/org/apache/daffodil/codegen/c/TestExNums.scala
+++ b/daffodil-test/src/test/scala/org/apache/daffodil/codegen/c/TestExNums.scala
@@ -38,6 +38,7 @@ class TestExNums {
   import TestExNums._
 
   @Test def s_ex_nums(): Unit = { runnerS.runOneTest("ex_nums") }
+  @Test def s_length(): Unit = { runnerS.runOneTest("length") }
   @Test def s_parse_error_off(): Unit = { runnerS.runOneTest("parse_error_off") }
   @Test def s_parse_error_limited(): Unit = { runnerS.runOneTest("parse_error_limited") }
   @Test def s_parse_error_on(): Unit = { runnerS.runOneTest("parse_error_on") }
@@ -46,6 +47,7 @@ class TestExNums {
   @Test def s_unparse_error_on(): Unit = { runnerS.runOneTest("unparse_error_on") }
 
   @Test def c_ex_nums(): Unit = { runnerC.runOneTest("ex_nums") }
+  @Test def c_length(): Unit = { runnerC.runOneTest("length") }
   @Test def c_parse_error_off(): Unit = { runnerC.runOneTest("parse_error_off") }
   @Test def c_parse_error_limited(): Unit = { runnerC.runOneTest("parse_error_limitedC") }
   @Test def c_parse_error_on(): Unit = { runnerC.runOneTest("parse_error_on") }