ART: Expression evaluation in Checker

It can be useful for tests to evaluate small `assert`-like expressions.
This patch adds such support to Checker, with a new CHECK-EVAL line.
See README file for more details.

Change-Id: I184f7c8e8b53f7e93cfb08fcf9630b4724fa5412
diff --git a/tools/checker/README b/tools/checker/README
index 259691e..65f5bd2 100644
--- a/tools/checker/README
+++ b/tools/checker/README
@@ -2,10 +2,10 @@
 state of the control-flow graph before and after each optimization pass
 against a set of assertions specified alongside the tests.
 
-Tests are written in Java, turned into DEX and compiled with the Optimizing
-compiler. "Check lines" are assertions formatted as comments of the Java file.
-They begin with prefix 'CHECK' followed by a pattern that the engine attempts
-to match in the compiler-generated output.
+Tests are written in Java or Smali, turned into DEX and compiled with the
+Optimizing compiler. "Check lines" are assertions formatted as comments of the
+source file. They begin with prefix "/// CHECK" or "## CHECK", respectively,
+followed by a pattern that the engine attempts to match in the compiler output.
 
 Assertions are tested in groups which correspond to the individual compiler
 passes. Each group of check lines therefore must start with a 'CHECK-START'
@@ -15,19 +15,23 @@
 
 Matching of check lines is carried out in the order of appearance in the
 source file. There are three types of check lines:
- - CHECK:     Must match an output line which appears in the output group
-              later than lines matched against any preceeding checks. Output
-              lines must therefore match the check lines in the same order.
-              These are referred to as "in-order" checks in the code.
- - CHECK-DAG: Must match an output line which appears in the output group
-              later than lines matched against any preceeding in-order checks.
-              In other words, the order of output lines does not matter
-              between consecutive DAG checks.
- - CHECK-NOT: Must not match any output line which appears in the output group
-              later than lines matched against any preceeding checks and
-              earlier than lines matched against any subsequent checks.
-              Surrounding non-negative checks (or boundaries of the group)
-              therefore create a scope within which the assertion is verified.
+ - CHECK:      Must match an output line which appears in the output group
+               later than lines matched against any preceeding checks. Output
+               lines must therefore match the check lines in the same order.
+               These are referred to as "in-order" checks in the code.
+ - CHECK-DAG:  Must match an output line which appears in the output group
+               later than lines matched against any preceeding in-order checks.
+               In other words, the order of output lines does not matter
+               between consecutive DAG checks.
+ - CHECK-NOT:  Must not match any output line which appears in the output group
+               later than lines matched against any preceeding checks and
+               earlier than lines matched against any subsequent checks.
+               Surrounding non-negative checks (or boundaries of the group)
+               therefore create a scope within which the assertion is verified.
+ - CHECK-NEXT: Must match the output line which comes right after the line which
+               matched the previous check. Cannot be used after any but the
+               in-order CHECK.
+ - CHECK-EVAL: Specifies a Python expression which must evaluate to 'True'.
 
 Check-line patterns are treated as plain text rather than regular expressions
 but are whitespace agnostic.
@@ -45,18 +49,30 @@
 Example:
   The following assertions can be placed in a Java source file:
 
-  // CHECK-START: int MyClass.MyMethod() constant_folding (after)
-  // CHECK:         <<ID:i\d+>>  IntConstant {{11|22}}
-  // CHECK:                      Return [<<ID>>]
+  /// CHECK-START: int MyClass.MyMethod() constant_folding (after)
+  /// CHECK:         <<ID:i\d+>>  IntConstant {{11|22}}
+  /// CHECK:                      Return [<<ID>>]
 
   The engine will attempt to match the check lines against the output of the
   group named on the first line. Together they verify that the CFG after
   constant folding returns an integer constant with value either 11 or 22.
 
+
+Of the language constructs above, 'CHECK-EVAL' lines support only referencing of
+variables. Any other surrounding text will be passed to Python's `eval` as is.
+
+Example:
+  /// CHECK-START: int MyClass.MyMethod() liveness (after)
+  /// CHECK:         InstructionA liveness:<<VarA:\d+>>
+  /// CHECK:         InstructionB liveness:<<VarB:\d+>>
+  /// CHECK-EVAL:    <<VarA>> != <<VarB>>
+
+
 A group of check lines can be made architecture-specific by inserting '-<arch>'
 after the 'CHECK-START' keyword. The previous example can be updated to run for
 arm64 only with:
 
-  // CHECK-START-ARM64: int MyClass.MyMethod() constant_folding (after)
-  // CHECK:         <<ID:i\d+>>  IntConstant {{11|22}}
-  // CHECK:                      Return [<<ID>>]
+Example:
+  /// CHECK-START-ARM64: int MyClass.MyMethod() constant_folding (after)
+  /// CHECK:         <<ID:i\d+>>  IntConstant {{11|22}}
+  /// CHECK:                      Return [<<ID>>]
diff --git a/tools/checker/file_format/checker/parser.py b/tools/checker/file_format/checker/parser.py
index 001f72a..446302f 100644
--- a/tools/checker/file_format/checker/parser.py
+++ b/tools/checker/file_format/checker/parser.py
@@ -15,7 +15,7 @@
 from common.archs               import archs_list
 from common.logger              import Logger
 from file_format.common         import SplitStream
-from file_format.checker.struct import CheckerFile, TestCase, TestAssertion, RegexExpression
+from file_format.checker.struct import CheckerFile, TestCase, TestAssertion, TestExpression
 
 import re
 
@@ -81,6 +81,11 @@
   if notLine is not None:
     return (notLine, TestAssertion.Variant.Not, lineNo), None, None
 
+  # 'CHECK-EVAL' lines evaluate a Python expression.
+  evalLine = __extractLine(prefix + "-EVAL", line)
+  if evalLine is not None:
+    return (evalLine, TestAssertion.Variant.Eval, lineNo), None, None
+
   Logger.fail("Checker assertion could not be parsed: '" + line + "'", fileName, lineNo)
 
 def __isMatchAtStart(match):
@@ -97,16 +102,24 @@
 
 def ParseCheckerAssertion(parent, line, variant, lineNo):
   """ This method parses the content of a check line stripped of the initial
-      comment symbol and the CHECK keyword.
+      comment symbol and the CHECK-* keyword.
   """
   assertion = TestAssertion(parent, variant, line, lineNo)
+  isEvalLine = (variant == TestAssertion.Variant.Eval)
+
   # Loop as long as there is something to parse.
   while line:
     # Search for the nearest occurrence of the special markers.
-    matchWhitespace = re.search(r"\s+", line)
-    matchPattern = re.search(RegexExpression.Regex.regexPattern, line)
-    matchVariableReference = re.search(RegexExpression.Regex.regexVariableReference, line)
-    matchVariableDefinition = re.search(RegexExpression.Regex.regexVariableDefinition, line)
+    if isEvalLine:
+      # The following constructs are not supported in CHECK-EVAL lines
+      matchWhitespace = None
+      matchPattern = None
+      matchVariableDefinition = None
+    else:
+      matchWhitespace = re.search(r"\s+", line)
+      matchPattern = re.search(TestExpression.Regex.regexPattern, line)
+      matchVariableDefinition = re.search(TestExpression.Regex.regexVariableDefinition, line)
+    matchVariableReference = re.search(TestExpression.Regex.regexVariableReference, line)
 
     # If one of the above was identified at the current position, extract them
     # from the line, parse them and add to the list of line parts.
@@ -114,24 +127,24 @@
       # A whitespace in the check line creates a new separator of line parts.
       # This allows for ignored output between the previous and next parts.
       line = line[matchWhitespace.end():]
-      assertion.addExpression(RegexExpression.createSeparator())
+      assertion.addExpression(TestExpression.createSeparator())
     elif __isMatchAtStart(matchPattern):
       pattern = line[0:matchPattern.end()]
       pattern = pattern[2:-2]
       line = line[matchPattern.end():]
-      assertion.addExpression(RegexExpression.createPattern(pattern))
+      assertion.addExpression(TestExpression.createPattern(pattern))
     elif __isMatchAtStart(matchVariableReference):
       var = line[0:matchVariableReference.end()]
       line = line[matchVariableReference.end():]
       name = var[2:-2]
-      assertion.addExpression(RegexExpression.createVariableReference(name))
+      assertion.addExpression(TestExpression.createVariableReference(name))
     elif __isMatchAtStart(matchVariableDefinition):
       var = line[0:matchVariableDefinition.end()]
       line = line[matchVariableDefinition.end():]
       colonPos = var.find(":")
       name = var[2:colonPos]
       body = var[colonPos+1:-2]
-      assertion.addExpression(RegexExpression.createVariableDefinition(name, body))
+      assertion.addExpression(TestExpression.createVariableDefinition(name, body))
     else:
       # If we're not currently looking at a special marker, this is a plain
       # text match all the way until the first special marker (or the end
@@ -143,7 +156,10 @@
                                 line)
       text = line[0:firstMatch]
       line = line[firstMatch:]
-      assertion.addExpression(RegexExpression.createText(text))
+      if isEvalLine:
+        assertion.addExpression(TestExpression.createPlainText(text))
+      else:
+        assertion.addExpression(TestExpression.createPatternFromPlainText(text))
   return assertion
 
 def ParseCheckerStream(fileName, prefix, stream):
diff --git a/tools/checker/file_format/checker/struct.py b/tools/checker/file_format/checker/struct.py
index 2b2e442..8181c4c 100644
--- a/tools/checker/file_format/checker/struct.py
+++ b/tools/checker/file_format/checker/struct.py
@@ -74,7 +74,7 @@
 
   class Variant(object):
     """Supported types of assertions."""
-    InOrder, NextLine, DAG, Not = range(4)
+    InOrder, NextLine, DAG, Not, Eval = range(5)
 
   def __init__(self, parent, variant, originalText, lineNo):
     assert isinstance(parent, TestCase)
@@ -92,9 +92,9 @@
     return self.parent.fileName
 
   def addExpression(self, new_expression):
-    assert isinstance(new_expression, RegexExpression)
+    assert isinstance(new_expression, TestExpression)
     if self.variant == TestAssertion.Variant.Not:
-      if new_expression.variant == RegexExpression.Variant.VarDef:
+      if new_expression.variant == TestExpression.Variant.VarDef:
         Logger.fail("CHECK-NOT lines cannot define variables", self.fileName, self.lineNo)
     self.expressions.append(new_expression)
 
@@ -102,10 +102,10 @@
     """ Returns a regex pattern for this entire assertion. Only used in tests. """
     regex = ""
     for expression in self.expressions:
-      if expression.variant == RegexExpression.Variant.Separator:
+      if expression.variant == TestExpression.Variant.Separator:
         regex = regex + ", "
       else:
-        regex = regex + "(" + expression.pattern + ")"
+        regex = regex + "(" + expression.text + ")"
     return regex
 
   def __eq__(self, other):
@@ -114,11 +114,11 @@
        and self.expressions == other.expressions
 
 
-class RegexExpression(EqualityMixin, PrintableMixin):
+class TestExpression(EqualityMixin, PrintableMixin):
 
   class Variant(object):
     """Supported language constructs."""
-    Text, Pattern, VarRef, VarDef, Separator = range(5)
+    PlainText, Pattern, VarRef, VarDef, Separator = range(5)
 
   class Regex(object):
     rName = r"([a-zA-Z][a-zA-Z0-9]*)"
@@ -131,37 +131,43 @@
 
     regexPattern = rPatternStartSym + rRegex + rPatternEndSym
     regexVariableReference = rVariableStartSym + rName + rVariableEndSym
-    regexVariableDefinition = rVariableStartSym + rName + rVariableSeparator + rRegex + rVariableEndSym
+    regexVariableDefinition = (rVariableStartSym +
+                                 rName + rVariableSeparator + rRegex +
+                               rVariableEndSym)
 
-  def __init__(self, variant, name, pattern):
+  def __init__(self, variant, name, text):
     self.variant = variant
     self.name = name
-    self.pattern = pattern
+    self.text = text
 
   def __eq__(self, other):
     return isinstance(other, self.__class__) \
        and self.variant == other.variant \
        and self.name == other.name \
-       and self.pattern == other.pattern
+       and self.text == other.text
 
   @staticmethod
   def createSeparator():
-    return RegexExpression(RegexExpression.Variant.Separator, None, None)
+    return TestExpression(TestExpression.Variant.Separator, None, None)
 
   @staticmethod
-  def createText(text):
-    return RegexExpression(RegexExpression.Variant.Text, None, re.escape(text))
+  def createPlainText(text):
+    return TestExpression(TestExpression.Variant.PlainText, None, text)
+
+  @staticmethod
+  def createPatternFromPlainText(text):
+    return TestExpression(TestExpression.Variant.Pattern, None, re.escape(text))
 
   @staticmethod
   def createPattern(pattern):
-    return RegexExpression(RegexExpression.Variant.Pattern, None, pattern)
+    return TestExpression(TestExpression.Variant.Pattern, None, pattern)
 
   @staticmethod
   def createVariableReference(name):
-    assert re.match(RegexExpression.Regex.rName, name)
-    return RegexExpression(RegexExpression.Variant.VarRef, name, None)
+    assert re.match(TestExpression.Regex.rName, name)
+    return TestExpression(TestExpression.Variant.VarRef, name, None)
 
   @staticmethod
   def createVariableDefinition(name, pattern):
-    assert re.match(RegexExpression.Regex.rName, name)
-    return RegexExpression(RegexExpression.Variant.VarDef, name, pattern)
+    assert re.match(TestExpression.Regex.rName, name)
+    return TestExpression(TestExpression.Variant.VarDef, name, pattern)
diff --git a/tools/checker/file_format/checker/test.py b/tools/checker/file_format/checker/test.py
index 36ed4b1..495dabc 100644
--- a/tools/checker/file_format/checker/test.py
+++ b/tools/checker/file_format/checker/test.py
@@ -17,7 +17,7 @@
 from common.archs               import archs_list
 from common.testing             import ToUnicode
 from file_format.checker.parser import ParseCheckerStream
-from file_format.checker.struct import CheckerFile, TestCase, TestAssertion, RegexExpression
+from file_format.checker.struct import CheckerFile, TestCase, TestAssertion, TestExpression
 
 import io
 import unittest
@@ -73,10 +73,11 @@
     self.assertParses("    ///CHECK: foo")
     self.assertParses("///    CHECK: foo")
 
-class CheckerParser_RegexExpressionTest(unittest.TestCase):
+class CheckerParser_TestExpressionTest(unittest.TestCase):
 
   def parseAssertion(self, string, variant=""):
-    checkerText = u"/// CHECK-START: pass\n/// CHECK" + ToUnicode(variant) + u": " + ToUnicode(string)
+    checkerText = (u"/// CHECK-START: pass\n" +
+                   u"/// CHECK" + ToUnicode(variant) + u": " + ToUnicode(string))
     checkerFile = ParseCheckerStream("<test-file>", "CHECK", io.StringIO(checkerText))
     self.assertEqual(len(checkerFile.testCases), 1)
     testCase = checkerFile.testCases[0]
@@ -92,17 +93,17 @@
     self.assertEqual(expected, self.parseAssertion(string).toRegex())
 
   def assertEqualsText(self, string, text):
-    self.assertEqual(self.parseExpression(string), RegexExpression.createText(text))
+    self.assertEqual(self.parseExpression(string), TestExpression.createPatternFromPlainText(text))
 
   def assertEqualsPattern(self, string, pattern):
-    self.assertEqual(self.parseExpression(string), RegexExpression.createPattern(pattern))
+    self.assertEqual(self.parseExpression(string), TestExpression.createPattern(pattern))
 
   def assertEqualsVarRef(self, string, name):
-    self.assertEqual(self.parseExpression(string), RegexExpression.createVariableReference(name))
+    self.assertEqual(self.parseExpression(string), TestExpression.createVariableReference(name))
 
   def assertEqualsVarDef(self, string, name, pattern):
     self.assertEqual(self.parseExpression(string),
-                     RegexExpression.createVariableDefinition(name, pattern))
+                     TestExpression.createVariableDefinition(name, pattern))
 
   def assertVariantNotEqual(self, string, variant):
     self.assertNotEqual(variant, self.parseExpression(string).variant)
@@ -166,17 +167,17 @@
     self.assertEqualsVarDef("<<ABC:(a[bc])>>", "ABC", "(a[bc])")
 
   def test_Empty(self):
-    self.assertVariantNotEqual("{{}}", RegexExpression.Variant.Pattern)
-    self.assertVariantNotEqual("<<>>", RegexExpression.Variant.VarRef)
-    self.assertVariantNotEqual("<<:>>", RegexExpression.Variant.VarDef)
+    self.assertEqualsText("{{}}", "{{}}")
+    self.assertVariantNotEqual("<<>>", TestExpression.Variant.VarRef)
+    self.assertVariantNotEqual("<<:>>", TestExpression.Variant.VarDef)
 
   def test_InvalidVarName(self):
-    self.assertVariantNotEqual("<<0ABC>>", RegexExpression.Variant.VarRef)
-    self.assertVariantNotEqual("<<AB=C>>", RegexExpression.Variant.VarRef)
-    self.assertVariantNotEqual("<<ABC=>>", RegexExpression.Variant.VarRef)
-    self.assertVariantNotEqual("<<0ABC:abc>>", RegexExpression.Variant.VarDef)
-    self.assertVariantNotEqual("<<AB=C:abc>>", RegexExpression.Variant.VarDef)
-    self.assertVariantNotEqual("<<ABC=:abc>>", RegexExpression.Variant.VarDef)
+    self.assertVariantNotEqual("<<0ABC>>", TestExpression.Variant.VarRef)
+    self.assertVariantNotEqual("<<AB=C>>", TestExpression.Variant.VarRef)
+    self.assertVariantNotEqual("<<ABC=>>", TestExpression.Variant.VarRef)
+    self.assertVariantNotEqual("<<0ABC:abc>>", TestExpression.Variant.VarDef)
+    self.assertVariantNotEqual("<<AB=C:abc>>", TestExpression.Variant.VarDef)
+    self.assertVariantNotEqual("<<ABC=:abc>>", TestExpression.Variant.VarDef)
 
   def test_BodyMatchNotGreedy(self):
     self.assertEqualsRegex("{{abc}}{{def}}", "(abc)(def)")
@@ -201,7 +202,7 @@
         content = assertionEntry[0]
         variant = assertionEntry[1]
         assertion = TestAssertion(testCase, variant, content, 0)
-        assertion.addExpression(RegexExpression.createText(content))
+        assertion.addExpression(TestExpression.createPatternFromPlainText(content))
     return testFile
 
   def assertParsesTo(self, checkerText, expectedData):
@@ -279,9 +280,15 @@
       self.parse(
         """
           /// CHECK-START: Example Group
+          /// CHECK-EVAL: foo
           /// CHECK-NEXT: bar
         """)
-
+    with self.assertRaises(CheckerException):
+      self.parse(
+        """
+          /// CHECK-START: Example Group
+          /// CHECK-NEXT: bar
+        """)
 
 class CheckerParser_ArchTests(unittest.TestCase):
 
@@ -329,3 +336,61 @@
       self.assertEqual(len(checkerFile.testCases), 1)
       self.assertEqual(len(checkerFile.testCasesForArch(arch)), 1)
       self.assertEqual(len(checkerFile.testCases[0].assertions), 4)
+
+
+class CheckerParser_EvalTests(unittest.TestCase):
+  def parseTestCase(self, string):
+    checkerText = u"/// CHECK-START: pass\n" + ToUnicode(string)
+    checkerFile = ParseCheckerStream("<test-file>", "CHECK", io.StringIO(checkerText))
+    self.assertEqual(len(checkerFile.testCases), 1)
+    return checkerFile.testCases[0]
+
+  def parseExpressions(self, string):
+    testCase = self.parseTestCase("/// CHECK-EVAL: " + string)
+    self.assertEqual(len(testCase.assertions), 1)
+    assertion = testCase.assertions[0]
+    self.assertEqual(assertion.variant, TestAssertion.Variant.Eval)
+    self.assertEqual(assertion.originalText, string)
+    return assertion.expressions
+
+  def assertParsesToPlainText(self, text):
+    testCase = self.parseTestCase("/// CHECK-EVAL: " + text)
+    self.assertEqual(len(testCase.assertions), 1)
+    assertion = testCase.assertions[0]
+    self.assertEqual(assertion.variant, TestAssertion.Variant.Eval)
+    self.assertEqual(assertion.originalText, text)
+    self.assertEqual(len(assertion.expressions), 1)
+    expression = assertion.expressions[0]
+    self.assertEqual(expression.variant, TestExpression.Variant.PlainText)
+    self.assertEqual(expression.text, text)
+
+  def test_PlainText(self):
+    self.assertParsesToPlainText("XYZ")
+    self.assertParsesToPlainText("True")
+    self.assertParsesToPlainText("{{abc}}")
+    self.assertParsesToPlainText("<<ABC:abc>>")
+    self.assertParsesToPlainText("<<ABC=>>")
+
+  def test_VariableReference(self):
+    self.assertEqual(self.parseExpressions("<<ABC>>"),
+                     [ TestExpression.createVariableReference("ABC") ])
+    self.assertEqual(self.parseExpressions("123<<ABC>>"),
+                     [ TestExpression.createPlainText("123"),
+                       TestExpression.createVariableReference("ABC") ])
+    self.assertEqual(self.parseExpressions("123  <<ABC>>"),
+                     [ TestExpression.createPlainText("123  "),
+                       TestExpression.createVariableReference("ABC") ])
+    self.assertEqual(self.parseExpressions("<<ABC>>XYZ"),
+                     [ TestExpression.createVariableReference("ABC"),
+                       TestExpression.createPlainText("XYZ") ])
+    self.assertEqual(self.parseExpressions("<<ABC>>   XYZ"),
+                     [ TestExpression.createVariableReference("ABC"),
+                       TestExpression.createPlainText("   XYZ") ])
+    self.assertEqual(self.parseExpressions("123<<ABC>>XYZ"),
+                     [ TestExpression.createPlainText("123"),
+                       TestExpression.createVariableReference("ABC"),
+                       TestExpression.createPlainText("XYZ") ])
+    self.assertEqual(self.parseExpressions("123 <<ABC>>  XYZ"),
+                     [ TestExpression.createPlainText("123 "),
+                       TestExpression.createVariableReference("ABC"),
+                       TestExpression.createPlainText("  XYZ") ])
diff --git a/tools/checker/match/file.py b/tools/checker/match/file.py
index 42ca7df..6601a1e 100644
--- a/tools/checker/match/file.py
+++ b/tools/checker/match/file.py
@@ -17,7 +17,7 @@
 from common.logger                    import Logger
 from file_format.c1visualizer.struct  import C1visualizerFile, C1visualizerPass
 from file_format.checker.struct       import CheckerFile, TestCase, TestAssertion
-from match.line                       import MatchLines
+from match.line                       import MatchLines, EvaluateLine
 
 MatchScope = namedtuple("MatchScope", ["start", "end"])
 MatchInfo = namedtuple("MatchInfo", ["scope", "variables"])
@@ -94,6 +94,11 @@
       if MatchLines(assertion, line, variables) is not None:
         raise MatchFailedException(assertion, i)
 
+def testEvalGroup(assertions, scope, variables):
+  for assertion in assertions:
+    if not EvaluateLine(assertion, variables):
+      raise MatchFailedException(assertion, scope.start)
+
 def MatchTestCase(testCase, c1Pass):
   """ Runs a test case against a C1visualizer graph dump.
 
@@ -132,11 +137,15 @@
       assert len(assertionGroup) == 1
       scope = MatchScope(matchFrom, matchFrom + 1)
       match = findMatchingLine(assertionGroup[0], c1Pass, scope, variables)
-    else:
+    elif assertionGroup[0].variant == TestAssertion.Variant.DAG:
       # A group of DAG assertions. Match them all starting from the same point.
-      assert assertionGroup[0].variant == TestAssertion.Variant.DAG
       scope = MatchScope(matchFrom, c1Length)
       match = matchDagGroup(assertionGroup, c1Pass, scope, variables)
+    else:
+      assert assertionGroup[0].variant == TestAssertion.Variant.Eval
+      scope = MatchScope(matchFrom, c1Length)
+      testEvalGroup(assertionGroup, scope, variables)
+      continue
 
     if pendingNotAssertions:
       # Previous group were NOT assertions. Make sure they don't match any lines
diff --git a/tools/checker/match/line.py b/tools/checker/match/line.py
index ce11e2a..08f001f 100644
--- a/tools/checker/match/line.py
+++ b/tools/checker/match/line.py
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 from common.logger              import Logger
-from file_format.checker.struct import RegexExpression
+from file_format.checker.struct import TestExpression, TestAssertion
 
 import re
 
@@ -21,30 +21,40 @@
   return list[0], list[1:]
 
 def splitAtSeparators(expressions):
-  """ Splits a list of RegexExpressions at separators. """
+  """ Splits a list of TestExpressions at separators. """
   splitExpressions = []
   wordStart = 0
   for index, expression in enumerate(expressions):
-    if expression.variant == RegexExpression.Variant.Separator:
+    if expression.variant == TestExpression.Variant.Separator:
       splitExpressions.append(expressions[wordStart:index])
       wordStart = index + 1
   splitExpressions.append(expressions[wordStart:])
   return splitExpressions
 
+def getVariable(name, variables, pos):
+  if name in variables:
+    return variables[name]
+  else:
+    Logger.testFailed("Missing definition of variable \"{}\"".format(name),
+                      pos.fileName, pos.lineNo)
+
+def setVariable(name, value, variables, pos):
+  if name not in variables:
+    return variables.copyWith(name, value)
+  else:
+    Logger.testFailed("Multiple definitions of variable \"{}\"".format(name),
+                      pos.fileName, pos.lineNo)
+
 def matchWords(checkerWord, stringWord, variables, pos):
-  """ Attempts to match a list of RegexExpressions against a string.
+  """ Attempts to match a list of TestExpressions against a string.
       Returns updated variable dictionary if successful and None otherwise.
   """
   for expression in checkerWord:
     # If `expression` is a variable reference, replace it with the value.
-    if expression.variant == RegexExpression.Variant.VarRef:
-      if expression.name in variables:
-        pattern = re.escape(variables[expression.name])
-      else:
-        Logger.testFailed("Missing definition of variable \"{}\"".format(expression.name),
-                          pos.fileName, pos.lineNo)
+    if expression.variant == TestExpression.Variant.VarRef:
+      pattern = re.escape(getVariable(expression.name, variables, pos))
     else:
-      pattern = expression.pattern
+      pattern = expression.text
 
     # Match the expression's regex pattern against the remainder of the word.
     # Note: re.match will succeed only if matched from the beginning.
@@ -53,12 +63,8 @@
       return None
 
     # If `expression` was a variable definition, set the variable's value.
-    if expression.variant == RegexExpression.Variant.VarDef:
-      if expression.name not in variables:
-        variables = variables.copyWith(expression.name, stringWord[:match.end()])
-      else:
-        Logger.testFailed("Multiple definitions of variable \"{}\"".format(expression.name),
-                          pos.fileName, pos.lineNo)
+    if expression.variant == TestExpression.Variant.VarDef:
+      variables = setVariable(expression.name, stringWord[:match.end()], variables, pos)
 
     # Move cursor by deleting the matched characters.
     stringWord = stringWord[match.end():]
@@ -73,11 +79,13 @@
   """ Attempts to match a CHECK line against a string. Returns variable state
       after the match if successful and None otherwise.
   """
+  assert checkerLine.variant != TestAssertion.Variant.Eval
+
   checkerWords = splitAtSeparators(checkerLine.expressions)
   stringWords = stringLine.split()
 
   while checkerWords:
-    # Get the next run of RegexExpressions which must match one string word.
+    # Get the next run of TestExpressions which must match one string word.
     checkerWord, checkerWords = headAndTail(checkerWords)
 
     # Keep reading words until a match is found.
@@ -92,5 +100,18 @@
     if not wordMatched:
       return None
 
-  # All RegexExpressions matched. Return new variable state.
+  # All TestExpressions matched. Return new variable state.
   return variables
+
+def getEvalText(expression, variables, pos):
+  if expression.variant == TestExpression.Variant.PlainText:
+    return expression.text
+  else:
+    assert expression.variant == TestExpression.Variant.VarRef
+    return getVariable(expression.name, variables, pos)
+
+def EvaluateLine(checkerLine, variables):
+  assert checkerLine.variant == TestAssertion.Variant.Eval
+  eval_string = "".join(map(lambda expr: getEvalText(expr, variables, checkerLine),
+                            checkerLine.expressions))
+  return eval(eval_string)
diff --git a/tools/checker/match/test.py b/tools/checker/match/test.py
index ca748c7..5144ca9 100644
--- a/tools/checker/match/test.py
+++ b/tools/checker/match/test.py
@@ -17,7 +17,7 @@
 from file_format.c1visualizer.parser import ParseC1visualizerStream
 from file_format.c1visualizer.struct import C1visualizerFile, C1visualizerPass
 from file_format.checker.parser      import ParseCheckerStream, ParseCheckerAssertion
-from file_format.checker.struct      import CheckerFile, TestCase, TestAssertion, RegexExpression
+from file_format.checker.struct      import CheckerFile, TestCase, TestAssertion
 from match.file                      import MatchTestCase, MatchFailedException
 from match.line                      import MatchLines
 
@@ -386,3 +386,17 @@
       abc
       bar
     """)
+
+  def test_EvalAssertions(self):
+    self.assertMatches("/// CHECK-EVAL: True", "foo")
+    self.assertDoesNotMatch("/// CHECK-EVAL: False", "foo")
+
+    self.assertMatches("/// CHECK-EVAL: 1 + 2 == 3", "foo")
+    self.assertDoesNotMatch("/// CHECK-EVAL: 1 + 2 == 4", "foo")
+
+    twoVarTestCase = """
+                       /// CHECK-DAG: <<X:\d+>> <<Y:\d+>>
+                       /// CHECK-EVAL: <<X>> > <<Y>>
+                     """
+    self.assertMatches(twoVarTestCase, "42 41");
+    self.assertDoesNotMatch(twoVarTestCase, "42 43")
diff --git a/tools/checker/run_unit_tests.py b/tools/checker/run_unit_tests.py
index 2f5b1fe..2e8f208 100755
--- a/tools/checker/run_unit_tests.py
+++ b/tools/checker/run_unit_tests.py
@@ -17,9 +17,10 @@
 from common.logger                 import Logger
 from file_format.c1visualizer.test import C1visualizerParser_Test
 from file_format.checker.test      import CheckerParser_PrefixTest, \
-                                          CheckerParser_RegexExpressionTest, \
+                                          CheckerParser_TestExpressionTest, \
                                           CheckerParser_FileLayoutTest, \
-                                          CheckerParser_ArchTests
+                                          CheckerParser_ArchTests, \
+                                          CheckerParser_EvalTests
 from match.test                    import MatchLines_Test, \
                                           MatchFiles_Test