parser.py revision b34c35ee8fe1516118d1d172a5e05b263ccbd93d
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, TestAssertion, TestExpression
19
20import re
21
22def __isCheckerLine(line):
23  return line.startswith("///") or line.startswith("##")
24
25def __extractLine(prefix, line, arch = None):
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  regexPrefix = rIgnoreWhitespace + \
34                r"(" + r"|".join(rCommentSymbols) + r")" + \
35                rIgnoreWhitespace + \
36                prefix + arch_specifier + r":"
37
38  # The 'match' function succeeds only if the pattern is matched at the
39  # beginning of the line.
40  match = re.match(regexPrefix, line)
41  if match is not None:
42    return line[match.end():].strip()
43  else:
44    return None
45
46def __processLine(line, lineNo, prefix, fileName):
47  """ This function is invoked on each line of the check file and returns a triplet
48      which instructs the parser how the line should be handled. If the line is
49      to be included in the current check group, it is returned in the first
50      value. If the line starts a new check group, the name of the group is
51      returned in the second value. The third value indicates whether the line
52      contained an architecture-specific suffix.
53  """
54  if not __isCheckerLine(line):
55    return None, None, None
56
57  # Lines beginning with 'CHECK-START' start a new test case.
58  # We currently only consider the architecture suffix in "CHECK-START" lines.
59  for arch in [None] + archs_list:
60    startLine = __extractLine(prefix + "-START", line, arch)
61    if startLine is not None:
62      return None, startLine, arch
63
64  # Lines starting only with 'CHECK' are matched in order.
65  plainLine = __extractLine(prefix, line)
66  if plainLine is not None:
67    return (plainLine, TestAssertion.Variant.InOrder, lineNo), None, None
68
69  # 'CHECK-NEXT' lines are in-order but must match the very next line.
70  nextLine = __extractLine(prefix + "-NEXT", line)
71  if nextLine is not None:
72    return (nextLine, TestAssertion.Variant.NextLine, lineNo), None, None
73
74  # 'CHECK-DAG' lines are no-order assertions.
75  dagLine = __extractLine(prefix + "-DAG", line)
76  if dagLine is not None:
77    return (dagLine, TestAssertion.Variant.DAG, lineNo), None, None
78
79  # 'CHECK-NOT' lines are no-order negative assertions.
80  notLine = __extractLine(prefix + "-NOT", line)
81  if notLine is not None:
82    return (notLine, TestAssertion.Variant.Not, lineNo), None, None
83
84  # 'CHECK-EVAL' lines evaluate a Python expression.
85  evalLine = __extractLine(prefix + "-EVAL", line)
86  if evalLine is not None:
87    return (evalLine, TestAssertion.Variant.Eval, lineNo), None, None
88
89  Logger.fail("Checker assertion could not be parsed: '" + line + "'", fileName, lineNo)
90
91def __isMatchAtStart(match):
92  """ Tests if the given Match occurred at the beginning of the line. """
93  return (match is not None) and (match.start() == 0)
94
95def __firstMatch(matches, string):
96  """ Takes in a list of Match objects and returns the minimal start point among
97      them. If there aren't any successful matches it returns the length of
98      the searched string.
99  """
100  starts = map(lambda m: len(string) if m is None else m.start(), matches)
101  return min(starts)
102
103def ParseCheckerAssertion(parent, line, variant, lineNo):
104  """ This method parses the content of a check line stripped of the initial
105      comment symbol and the CHECK-* keyword.
106  """
107  assertion = TestAssertion(parent, variant, line, lineNo)
108  isEvalLine = (variant == TestAssertion.Variant.Eval)
109
110  # Loop as long as there is something to parse.
111  while line:
112    # Search for the nearest occurrence of the special markers.
113    if isEvalLine:
114      # The following constructs are not supported in CHECK-EVAL lines
115      matchWhitespace = None
116      matchPattern = None
117      matchVariableDefinition = None
118    else:
119      matchWhitespace = re.search(r"\s+", line)
120      matchPattern = re.search(TestExpression.Regex.regexPattern, line)
121      matchVariableDefinition = re.search(TestExpression.Regex.regexVariableDefinition, line)
122    matchVariableReference = re.search(TestExpression.Regex.regexVariableReference, line)
123
124    # If one of the above was identified at the current position, extract them
125    # from the line, parse them and add to the list of line parts.
126    if __isMatchAtStart(matchWhitespace):
127      # A whitespace in the check line creates a new separator of line parts.
128      # This allows for ignored output between the previous and next parts.
129      line = line[matchWhitespace.end():]
130      assertion.addExpression(TestExpression.createSeparator())
131    elif __isMatchAtStart(matchPattern):
132      pattern = line[0:matchPattern.end()]
133      pattern = pattern[2:-2]
134      line = line[matchPattern.end():]
135      assertion.addExpression(TestExpression.createPattern(pattern))
136    elif __isMatchAtStart(matchVariableReference):
137      var = line[0:matchVariableReference.end()]
138      line = line[matchVariableReference.end():]
139      name = var[2:-2]
140      assertion.addExpression(TestExpression.createVariableReference(name))
141    elif __isMatchAtStart(matchVariableDefinition):
142      var = line[0:matchVariableDefinition.end()]
143      line = line[matchVariableDefinition.end():]
144      colonPos = var.find(":")
145      name = var[2:colonPos]
146      body = var[colonPos+1:-2]
147      assertion.addExpression(TestExpression.createVariableDefinition(name, body))
148    else:
149      # If we're not currently looking at a special marker, this is a plain
150      # text match all the way until the first special marker (or the end
151      # of the line).
152      firstMatch = __firstMatch([ matchWhitespace,
153                                  matchPattern,
154                                  matchVariableReference,
155                                  matchVariableDefinition ],
156                                line)
157      text = line[0:firstMatch]
158      line = line[firstMatch:]
159      if isEvalLine:
160        assertion.addExpression(TestExpression.createPlainText(text))
161      else:
162        assertion.addExpression(TestExpression.createPatternFromPlainText(text))
163  return assertion
164
165def ParseCheckerStream(fileName, prefix, stream):
166  checkerFile = CheckerFile(fileName)
167  fnProcessLine = lambda line, lineNo: __processLine(line, lineNo, prefix, fileName)
168  fnLineOutsideChunk = lambda line, lineNo: \
169      Logger.fail("Checker line not inside a group", fileName, lineNo)
170  for caseName, caseLines, startLineNo, testArch in \
171      SplitStream(stream, fnProcessLine, fnLineOutsideChunk):
172    testCase = TestCase(checkerFile, caseName, startLineNo, testArch)
173    for caseLine in caseLines:
174      ParseCheckerAssertion(testCase, caseLine[0], caseLine[1], caseLine[2])
175  return checkerFile
176