1# Copyright (C) 2014 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#   http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15from common.archs               import archs_list
16from common.logger              import Logger
17from file_format.common         import SplitStream
18from file_format.checker.struct import CheckerFile, TestCase, TestStatement, TestExpression
19
20import re
21
22def __isCheckerLine(line):
23  return line.startswith("///") or line.startswith("##")
24
25def __extractLine(prefix, line, arch = None, debuggable = False):
26  """ Attempts to parse a check line. The regex searches for a comment symbol
27      followed by the CHECK keyword, given attribute and a colon at the very
28      beginning of the line. Whitespaces are ignored.
29  """
30  rIgnoreWhitespace = r"\s*"
31  rCommentSymbols = [r"///", r"##"]
32  arch_specifier = r"-%s" % arch if arch is not None else r""
33  dbg_specifier = r"-DEBUGGABLE" if debuggable else r""
34  regexPrefix = rIgnoreWhitespace + \
35                r"(" + r"|".join(rCommentSymbols) + r")" + \
36                rIgnoreWhitespace + \
37                prefix + arch_specifier + dbg_specifier + r":"
38
39  # The 'match' function succeeds only if the pattern is matched at the
40  # beginning of the line.
41  match = re.match(regexPrefix, line)
42  if match is not None:
43    return line[match.end():].strip()
44  else:
45    return None
46
47def __preprocessLineForStart(prefix, line, targetArch):
48  """ This function modifies a CHECK-START-{x,y,z} into a matching
49      CHECK-START-y line for matching targetArch y. If no matching
50      architecture is found, CHECK-START-x is returned arbitrarily
51      to ensure all following check lines are put into a test that
52      is skipped. Any other line is left unmodified.
53  """
54  if targetArch is not None:
55    if prefix in line:
56      # Find { } on the line and assume that defines the set.
57      s = line.find('{')
58      e = line.find('}')
59      if 0 < s and s < e:
60        archs = line[s+1:e].split(',')
61        # First verify that every archs is valid. Return the
62        # full line on failure to prompt error back to user.
63        for arch in archs:
64          if not arch in archs_list:
65            return line
66        # Now accept matching arch or arbitrarily return first.
67        if targetArch in archs:
68          return line[:s] + targetArch + line[e + 1:]
69        else:
70          return line[:s] + archs[0] + line[e + 1:]
71  return line
72
73def __processLine(line, lineNo, prefix, fileName, targetArch):
74  """ This function is invoked on each line of the check file and returns a triplet
75      which instructs the parser how the line should be handled. If the line is
76      to be included in the current check group, it is returned in the first
77      value. If the line starts a new check group, the name of the group is
78      returned in the second value. The third value indicates whether the line
79      contained an architecture-specific suffix.
80  """
81  if not __isCheckerLine(line):
82    return None, None, None
83
84  # Lines beginning with 'CHECK-START' start a new test case.
85  # We currently only consider the architecture suffix(es) in "CHECK-START" lines.
86  for debuggable in [True, False]:
87    sline = __preprocessLineForStart(prefix + "-START", line, targetArch)
88    for arch in [None] + archs_list:
89      startLine = __extractLine(prefix + "-START", sline, arch, debuggable)
90      if startLine is not None:
91        return None, startLine, (arch, debuggable)
92
93  # Lines starting only with 'CHECK' are matched in order.
94  plainLine = __extractLine(prefix, line)
95  if plainLine is not None:
96    return (plainLine, TestStatement.Variant.InOrder, lineNo), None, None
97
98  # 'CHECK-NEXT' lines are in-order but must match the very next line.
99  nextLine = __extractLine(prefix + "-NEXT", line)
100  if nextLine is not None:
101    return (nextLine, TestStatement.Variant.NextLine, lineNo), None, None
102
103  # 'CHECK-DAG' lines are no-order statements.
104  dagLine = __extractLine(prefix + "-DAG", line)
105  if dagLine is not None:
106    return (dagLine, TestStatement.Variant.DAG, lineNo), None, None
107
108  # 'CHECK-NOT' lines are no-order negative statements.
109  notLine = __extractLine(prefix + "-NOT", line)
110  if notLine is not None:
111    return (notLine, TestStatement.Variant.Not, lineNo), None, None
112
113  # 'CHECK-EVAL' lines evaluate a Python expression.
114  evalLine = __extractLine(prefix + "-EVAL", line)
115  if evalLine is not None:
116    return (evalLine, TestStatement.Variant.Eval, lineNo), None, None
117
118  # 'CHECK-IF' lines mark the beginning of a block that will be executed
119  # only if the Python expression that follows evaluates to true.
120  ifLine = __extractLine(prefix + "-IF", line)
121  if ifLine is not None:
122    return (ifLine, TestStatement.Variant.If, lineNo), None, None
123
124  # 'CHECK-ELIF' lines mark the beginning of an `else if` branch of a CHECK-IF block.
125  elifLine = __extractLine(prefix + "-ELIF", line)
126  if elifLine is not None:
127    return (elifLine, TestStatement.Variant.Elif, lineNo), None, None
128
129  # 'CHECK-ELSE' lines mark the beginning of the `else` branch of a CHECK-IF block.
130  elseLine = __extractLine(prefix + "-ELSE", line)
131  if elseLine is not None:
132    return (elseLine, TestStatement.Variant.Else, lineNo), None, None
133
134  # 'CHECK-FI' lines mark the end of a CHECK-IF block.
135  fiLine = __extractLine(prefix + "-FI", line)
136  if fiLine is not None:
137    return (fiLine, TestStatement.Variant.Fi, lineNo), None, None
138
139  Logger.fail("Checker statement could not be parsed: '" + line + "'", fileName, lineNo)
140
141def __isMatchAtStart(match):
142  """ Tests if the given Match occurred at the beginning of the line. """
143  return (match is not None) and (match.start() == 0)
144
145def __firstMatch(matches, string):
146  """ Takes in a list of Match objects and returns the minimal start point among
147      them. If there aren't any successful matches it returns the length of
148      the searched string.
149  """
150  starts = map(lambda m: len(string) if m is None else m.start(), matches)
151  return min(starts)
152
153def ParseCheckerStatement(parent, line, variant, lineNo):
154  """ This method parses the content of a check line stripped of the initial
155      comment symbol and the CHECK-* keyword.
156  """
157  statement = TestStatement(parent, variant, line, lineNo)
158
159  if statement.isNoContentStatement() and line:
160    Logger.fail("Expected empty statement: '" + line + "'", statement.fileName, statement.lineNo)
161
162  # Loop as long as there is something to parse.
163  while line:
164    # Search for the nearest occurrence of the special markers.
165    if statement.isEvalContentStatement():
166      # The following constructs are not supported in CHECK-EVAL, -IF and -ELIF lines
167      matchWhitespace = None
168      matchPattern = None
169      matchVariableDefinition = None
170    else:
171      matchWhitespace = re.search(r"\s+", line)
172      matchPattern = re.search(TestExpression.Regex.regexPattern, line)
173      matchVariableDefinition = re.search(TestExpression.Regex.regexVariableDefinition, line)
174    matchVariableReference = re.search(TestExpression.Regex.regexVariableReference, line)
175
176    # If one of the above was identified at the current position, extract them
177    # from the line, parse them and add to the list of line parts.
178    if __isMatchAtStart(matchWhitespace):
179      # A whitespace in the check line creates a new separator of line parts.
180      # This allows for ignored output between the previous and next parts.
181      line = line[matchWhitespace.end():]
182      statement.addExpression(TestExpression.createSeparator())
183    elif __isMatchAtStart(matchPattern):
184      pattern = line[0:matchPattern.end()]
185      pattern = pattern[2:-2]
186      line = line[matchPattern.end():]
187      statement.addExpression(TestExpression.createPattern(pattern))
188    elif __isMatchAtStart(matchVariableReference):
189      var = line[0:matchVariableReference.end()]
190      line = line[matchVariableReference.end():]
191      name = var[2:-2]
192      statement.addExpression(TestExpression.createVariableReference(name))
193    elif __isMatchAtStart(matchVariableDefinition):
194      var = line[0:matchVariableDefinition.end()]
195      line = line[matchVariableDefinition.end():]
196      colonPos = var.find(":")
197      name = var[2:colonPos]
198      body = var[colonPos+1:-2]
199      statement.addExpression(TestExpression.createVariableDefinition(name, body))
200    else:
201      # If we're not currently looking at a special marker, this is a plain
202      # text match all the way until the first special marker (or the end
203      # of the line).
204      firstMatch = __firstMatch([ matchWhitespace,
205                                  matchPattern,
206                                  matchVariableReference,
207                                  matchVariableDefinition ],
208                                line)
209      text = line[0:firstMatch]
210      line = line[firstMatch:]
211      if statement.isEvalContentStatement():
212        statement.addExpression(TestExpression.createPlainText(text))
213      else:
214        statement.addExpression(TestExpression.createPatternFromPlainText(text))
215  return statement
216
217def ParseCheckerStream(fileName, prefix, stream, targetArch = None):
218  checkerFile = CheckerFile(fileName)
219  fnProcessLine = lambda line, lineNo: __processLine(line, lineNo, prefix, fileName, targetArch)
220  fnLineOutsideChunk = lambda line, lineNo: \
221      Logger.fail("Checker line not inside a group", fileName, lineNo)
222  for caseName, caseLines, startLineNo, testData in \
223      SplitStream(stream, fnProcessLine, fnLineOutsideChunk):
224    testArch = testData[0]
225    forDebuggable = testData[1]
226    testCase = TestCase(checkerFile, caseName, startLineNo, testArch, forDebuggable)
227    for caseLine in caseLines:
228      ParseCheckerStatement(testCase, caseLine[0], caseLine[1], caseLine[2])
229  return checkerFile
230