Checker: Add function isaHasFeature

Developers are now able to use hasIsaFeature("feature_name") to
check if an instruction set feature was used at compile time.

Checker will retrieve the list of features from the .cfg file. It
expects them to be dumped at the beginning of the file as a fake
compilation block in the following form:

begin_compilation
  name "isa_features:feature1,-feature2"
  method "isa_features:feature1,-feature2"
  date 1580721972
end_compilation

Dumping that is optional. hasIsaFeature() will always return False
if that pass is not found.

Author:    Fabio Rinaldi
Committer: Artem Serov

Bug: 147876827
Test: ./art/tools/checker/run_unit_tests.py
Test: test.py --target --optimizing
Change-Id: I4ce15d853025f9863d7981b33b761cfc799fed50
diff --git a/tools/checker/README b/tools/checker/README
index 51be828..b04b0d8 100644
--- a/tools/checker/README
+++ b/tools/checker/README
@@ -93,9 +93,23 @@
  - CHECK-FI:
 
 CHECK-IF and CHECK-ELIF take a Python expression as input that will be evaluated by `eval`.
-Like CHECK-EVAL, they support only referencing of variables, defining new variables as part
-of the statement input is not allowed. Any other surrounding text will be passed to Python's `eval`
-as is. CHECK-ELSE and CHECK-FI must not have any input.
+
+A possible use case of branching is to check whether the generated code exploits the instruction
+architecture features enabled at compile time. For that purpose, you can call the custom made
+function isaHasFeature("feature_name").
+
+Example:
+  /// CHECK-START-ARM64: int other.TestByte.testDotProdComplex(byte[], byte[]) disassembly (after)
+  /// CHECK:        VecDotProd
+  /// CHECK-IF:     isaHasFeature("dotprod")
+  ///               CHECK:        sdot
+  /// CHECK-ELSE:
+  ///               CHECK-NOT:    sdot
+  /// CHECK-FI:
+
+Like CHECK-EVAL, CHECK-IF and CHECK-ELIF support only referencing of variables, defining new
+variables as part of the statement input is not allowed. Any other surrounding text will be passed
+to Python's `eval` as is. CHECK-ELSE and CHECK-FI must not have any input.
 
 Example:
   /// CHECK-START: int MyClass.MyMethod() constant_folding (after)
diff --git a/tools/checker/common/logger.py b/tools/checker/common/logger.py
index 15eb55c..aa3a92f 100644
--- a/tools/checker/common/logger.py
+++ b/tools/checker/common/logger.py
@@ -44,14 +44,14 @@
   Verbosity = Level.Info
 
   @staticmethod
-  def log(text, level=Level.Info, color=Color.Default, newLine=True, out=sys.stdout):
+  def log(content, level=Level.Info, color=Color.Default, newLine=True, out=sys.stdout):
     if level <= Logger.Verbosity:
-      text = Logger.Color.terminalCode(color, out) + text + \
+      content = Logger.Color.terminalCode(color, out) + str(content) + \
              Logger.Color.terminalCode(Logger.Color.Default, out)
       if newLine:
-        print(text, file=out)
+        print(content, file=out)
       else:
-        print(text, end="", file=out)
+        print(content, end="", file=out)
       out.flush()
 
   @staticmethod
diff --git a/tools/checker/file_format/c1visualizer/parser.py b/tools/checker/file_format/c1visualizer/parser.py
index bdcde9d..e16382e 100644
--- a/tools/checker/file_format/c1visualizer/parser.py
+++ b/tools/checker/file_format/c1visualizer/parser.py
@@ -25,7 +25,7 @@
     self.currentState = C1ParserState.OutsideBlock
     self.lastMethodName = None
 
-def __parseC1Line(line, lineNo, state, fileName):
+def __parseC1Line(c1File, line, lineNo, state, fileName):
   """ This function is invoked on each line of the output file and returns
       a triplet which instructs the parser how the line should be handled. If the
       line is to be included in the current group, it is returned in the first
@@ -58,7 +58,25 @@
       methodName = line.split("\"")[1].strip()
       if not methodName:
         Logger.fail("Empty method name in output", fileName, lineNo)
-      state.lastMethodName = methodName
+
+      m = re.match("isa_features:([\w,-]+)", methodName)
+      if (m):
+        rawFeatures = m.group(1).split(",")
+        # Create a map of features in the form {featureName: isEnabled}.
+        features = {}
+        for rf in rawFeatures:
+          featureName = rf
+          isEnabled = True
+          # A '-' in front of the feature name indicates that the feature wasn't enabled at compile
+          # time.
+          if rf[0] == '-':
+            featureName = rf[1:]
+            isEnabled = False
+          features[featureName] = isEnabled
+
+        c1File.setISAFeatures(features)
+      else:
+        state.lastMethodName = methodName
     elif line == "end_compilation":
       state.currentState = C1ParserState.OutsideBlock
     return (None, None, None)
@@ -81,7 +99,7 @@
 def ParseC1visualizerStream(fileName, stream):
   c1File = C1visualizerFile(fileName)
   state = C1ParserState()
-  fnProcessLine = lambda line, lineNo: __parseC1Line(line, lineNo, state, fileName)
+  fnProcessLine = lambda line, lineNo: __parseC1Line(c1File, line, lineNo, state, fileName)
   fnLineOutsideChunk = lambda line, lineNo: \
       Logger.fail("C1visualizer line not inside a group", fileName, lineNo)
   for passName, passLines, startLineNo, testArch in \
diff --git a/tools/checker/file_format/c1visualizer/struct.py b/tools/checker/file_format/c1visualizer/struct.py
index 991564e..21036da 100644
--- a/tools/checker/file_format/c1visualizer/struct.py
+++ b/tools/checker/file_format/c1visualizer/struct.py
@@ -12,14 +12,19 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from common.logger import Logger
-from common.mixins import PrintableMixin
+from common.immutables import ImmutableDict
+from common.logger     import Logger
+from common.mixins     import PrintableMixin
 
 class C1visualizerFile(PrintableMixin):
 
   def __init__(self, fileName):
     self.fileName = fileName
     self.passes = []
+    self.instructionSetFeatures = ImmutableDict()
+
+  def setISAFeatures(self, features):
+    self.instructionSetFeatures = ImmutableDict(features)
 
   def addPass(self, new_pass):
     self.passes.append(new_pass)
@@ -32,7 +37,8 @@
 
   def __eq__(self, other):
     return isinstance(other, self.__class__) \
-       and self.passes == other.passes
+       and self.passes == other.passes \
+       and self.instructionSetFeatures == other.instructionSetFeatures
 
 
 class C1visualizerPass(PrintableMixin):
diff --git a/tools/checker/file_format/c1visualizer/test.py b/tools/checker/file_format/c1visualizer/test.py
index 812a4cf..11a6f0e 100644
--- a/tools/checker/file_format/c1visualizer/test.py
+++ b/tools/checker/file_format/c1visualizer/test.py
@@ -14,6 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from common.immutables               import ImmutableDict
 from common.testing                  import ToUnicode
 from file_format.c1visualizer.parser import ParseC1visualizerStream
 from file_format.c1visualizer.struct import C1visualizerFile, C1visualizerPass
@@ -23,13 +24,16 @@
 
 class C1visualizerParser_Test(unittest.TestCase):
 
-  def createFile(self, passList):
+  def createFile(self, data):
     """ Creates an instance of CheckerFile from provided info.
 
-    Data format: [ ( <case-name>, [ ( <text>, <assert-variant> ), ... ] ), ... ]
+    Data format: ( [ <isa-feature>, ... ],
+                   [ ( <case-name>, [ ( <text>, <assert-variant> ), ... ] ), ... ]
+                 )
     """
     c1File = C1visualizerFile("<c1_file>")
-    for passEntry in passList:
+    c1File.instructionSetFeatures = data[0]
+    for passEntry in data[1]:
       passName = passEntry[0]
       passBody = passEntry[1]
       c1Pass = C1visualizerPass(c1File, passName, passBody, 0)
@@ -41,7 +45,7 @@
     return self.assertEqual(expectedFile, actualFile)
 
   def test_EmptyFile(self):
-    self.assertParsesTo("", [])
+    self.assertParsesTo("", (ImmutableDict(), []))
 
   def test_SingleGroup(self):
     self.assertParsesTo(
@@ -55,7 +59,9 @@
           bar
         end_cfg
       """,
-      [ ( "MyMethod pass1", [ "foo", "bar" ] ) ])
+      ( ImmutableDict(), [
+        ( "MyMethod pass1", [ "foo", "bar" ] )
+      ]))
 
   def test_MultipleGroups(self):
     self.assertParsesTo(
@@ -76,8 +82,10 @@
           def
         end_cfg
       """,
-      [ ( "MyMethod1 pass1", [ "foo", "bar" ] ),
-        ( "MyMethod1 pass2", [ "abc", "def" ] ) ])
+      ( ImmutableDict(), [
+        ( "MyMethod1 pass1", [ "foo", "bar" ] ),
+        ( "MyMethod1 pass2", [ "abc", "def" ] )
+      ]))
     self.assertParsesTo(
       """
         begin_compilation
@@ -101,5 +109,39 @@
           def
         end_cfg
       """,
-      [ ( "MyMethod1 pass1", [ "foo", "bar" ] ),
-        ( "MyMethod2 pass2", [ "abc", "def" ] ) ])
+      ( ImmutableDict(), [
+        ( "MyMethod1 pass1", [ "foo", "bar" ] ),
+        ( "MyMethod2 pass2", [ "abc", "def" ] )
+      ]))
+
+  def test_InstructionSetFeatures(self):
+    self.assertParsesTo(
+      """
+        begin_compilation
+          name "isa_features:feature1,-feature2"
+          method "isa_features:feature1,-feature2"
+          date 1234
+        end_compilation
+      """,
+      ( ImmutableDict({"feature1": True, "feature2": False}), []))
+    self.assertParsesTo(
+      """
+        begin_compilation
+          name "isa_features:feature1,-feature2"
+          method "isa_features:feature1,-feature2"
+          date 1234
+        end_compilation
+        begin_compilation
+          name "xyz1"
+          method "MyMethod1"
+          date 1234
+        end_compilation
+        begin_cfg
+          name "pass1"
+          foo
+          bar
+        end_cfg
+      """,
+      ( ImmutableDict({"feature1": True, "feature2": False}), [
+        ( "MyMethod1 pass1", [ "foo", "bar" ] )
+      ]))
diff --git a/tools/checker/match/file.py b/tools/checker/match/file.py
index 37d0195..5c6aed7 100644
--- a/tools/checker/match/file.py
+++ b/tools/checker/match/file.py
@@ -311,14 +311,15 @@
 
     self.lastVariant = variant
 
-def MatchTestCase(testCase, c1Pass):
+def MatchTestCase(testCase, c1Pass, instructionSetFeatures):
   """ Runs a test case against a C1visualizer graph dump.
 
   Raises MatchFailedException when a statement cannot be satisfied.
   """
   assert testCase.name == c1Pass.name
 
-  state = ExecutionState(c1Pass)
+  initialVariables = {"ISA_FEATURES": instructionSetFeatures}
+  state = ExecutionState(c1Pass, initialVariables)
   testStatements = testCase.statements + [ None ]
   for statement in testStatements:
     state.handle(statement)
@@ -342,7 +343,7 @@
 
     Logger.startTest(testCase.name)
     try:
-      MatchTestCase(testCase, c1Pass)
+      MatchTestCase(testCase, c1Pass, c1File.instructionSetFeatures)
       Logger.testPassed()
     except MatchFailedException as e:
       lineNo = c1Pass.startLineNo + e.lineNo
diff --git a/tools/checker/match/line.py b/tools/checker/match/line.py
index 49b55de..91450a5 100644
--- a/tools/checker/match/line.py
+++ b/tools/checker/match/line.py
@@ -111,6 +111,7 @@
 
 def EvaluateLine(checkerLine, variables):
   assert checkerLine.isEvalContentStatement()
+  hasIsaFeature = lambda feature: variables["ISA_FEATURES"].get(feature, False)
   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 01724f0..69e515f 100644
--- a/tools/checker/match/test.py
+++ b/tools/checker/match/test.py
@@ -103,12 +103,27 @@
 
 class MatchFiles_Test(unittest.TestCase):
 
-  def assertMatches(self, checkerString, c1String):
+  def assertMatches(self, checkerString, c1String, instructionSetFeatures=None):
     checkerString = \
       """
         /// CHECK-START: MyMethod MyPass
       """ + checkerString
-    c1String = \
+    featureString = ""
+    if instructionSetFeatures:
+      joinedFeatures = ",".join(map(lambda feature: feature
+                                                    if instructionSetFeatures[feature]
+                                                    else "-" + feature,
+                                    instructionSetFeatures))
+
+      featureString = \
+        """
+          begin_compilation
+            name "isa_features:%s"
+            method "isa_features:%s"
+            date 1234
+          end_compilation
+        """ % (joinedFeatures, joinedFeatures)
+    c1String = featureString + \
       """
         begin_compilation
           name "MyMethod"
@@ -125,11 +140,11 @@
     c1File = ParseC1visualizerStream("<c1-file>", io.StringIO(ToUnicode(c1String)))
     assert len(checkerFile.testCases) == 1
     assert len(c1File.passes) == 1
-    MatchTestCase(checkerFile.testCases[0], c1File.passes[0])
+    MatchTestCase(checkerFile.testCases[0], c1File.passes[0], c1File.instructionSetFeatures)
 
-  def assertDoesNotMatch(self, checkerString, c1String):
+  def assertDoesNotMatch(self, checkerString, c1String, instructionSetFeatures=None):
     with self.assertRaises(MatchFailedException):
-      self.assertMatches(checkerString, c1String)
+      self.assertMatches(checkerString, c1String, instructionSetFeatures)
 
   def assertBadStructure(self, checkerString, c1String):
     with self.assertRaises(BadStructureException):
@@ -915,4 +930,39 @@
       """,
       """
       foo
-      """)
\ No newline at end of file
+      """)
+
+  def test_hasIsaFeature(self):
+    self.assertMatches(
+      """
+        /// CHECK-EVAL: hasIsaFeature('feature1') and not hasIsaFeature('feature2')
+      """,
+      """
+      foo
+      """,
+      ImmutableDict({"feature1": True})
+    )
+    self.assertDoesNotMatch(
+      """
+        /// CHECK-EVAL: not hasIsaFeature('feature1')
+      """,
+      """
+      foo
+      """,
+      ImmutableDict({"feature1": True})
+    )
+    self.assertMatches(
+      """
+        /// CHECK-IF: hasIsaFeature('feature2')
+        ///   CHECK: bar1
+        /// CHECK-ELSE:
+        ///   CHECK: bar2
+        /// CHECK-FI:
+      """,
+      """
+      foo
+      bar1
+      """,
+      ImmutableDict({"feature1": False, "feature2": True})
+    )
+