You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
367 lines
14 KiB
367 lines
14 KiB
# Copyright (C) 2014 The Android Open Source Project
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
from collections import namedtuple
|
|
|
|
from common.immutables import ImmutableDict
|
|
from common.logger import Logger
|
|
from file_format.checker.struct import TestStatement
|
|
from match.line import match_lines, evaluate_line
|
|
|
|
MatchScope = namedtuple("MatchScope", ["start", "end"])
|
|
MatchInfo = namedtuple("MatchInfo", ["scope", "variables"])
|
|
|
|
|
|
class MatchFailedException(Exception):
|
|
def __init__(self, statement, line_no, variables):
|
|
self.statement = statement
|
|
self.line_no = line_no
|
|
self.variables = variables
|
|
|
|
|
|
class BadStructureException(Exception):
|
|
def __init__(self, msg, line_no):
|
|
self.msg = msg
|
|
self.line_no = line_no
|
|
|
|
|
|
class IfStack:
|
|
"""
|
|
The purpose of this class is to keep track of which branch the cursor is in.
|
|
This will let us know if the line read by the cursor should be processed or not.
|
|
Furthermore, this class contains the methods to handle the CHECK-[IF, ELIF, ELSE, FI]
|
|
statements, and consequently update the stack with new information.
|
|
|
|
The following elements can appear on the stack:
|
|
- BRANCH_TAKEN: a branch is taken if its condition evaluates to true and
|
|
its parent branch was also previously taken.
|
|
- BRANCH_NOT_TAKEN_YET: the branch's parent was taken, but this branch wasn't as its
|
|
condition did not evaluate to true.
|
|
- BRANCH_NOT_TAKEN: a branch is not taken when its parent was either NotTaken or NotTakenYet.
|
|
It doesn't matter if the condition would evaluate to true, that's not even checked.
|
|
|
|
CHECK-IF is the only instruction that pushes a new element on the stack. CHECK-ELIF
|
|
and CHECK-ELSE will update the top of the stack to keep track of what's been seen.
|
|
That means that we can check if the line currently pointed to by the cursor should be
|
|
processed just by looking at the top of the stack.
|
|
CHECK-FI will pop the last element.
|
|
|
|
`BRANCH_TAKEN`, `BRANCH_NOT_TAKEN`, `BRANCH_NOT_TAKEN_YET` are implemented as positive integers.
|
|
Negated values of `BRANCH_TAKEN` and `BRANCH_NOT_TAKEN` may be appear; `-BRANCH_TAKEN` and
|
|
`-BRANCH_NOT_TAKEN` have the same meaning as `BRANCH_TAKEN` and `BRANCH_NOT_TAKEN`
|
|
(respectively), but they indicate that we went past the ELSE branch. Knowing that, we can
|
|
output a precise error message if the user creates a malformed branching structure.
|
|
"""
|
|
|
|
BRANCH_TAKEN, BRANCH_NOT_TAKEN, BRANCH_NOT_TAKEN_YET = range(1, 4)
|
|
|
|
def __init__(self):
|
|
self.stack = []
|
|
|
|
def can_execute(self):
|
|
"""
|
|
Returns true if we're not in any branch, or the branch we're
|
|
currently in was taken.
|
|
"""
|
|
if self._is_empty():
|
|
return True
|
|
return abs(self._peek()) == IfStack.BRANCH_TAKEN
|
|
|
|
def handle(self, statement, variables):
|
|
"""
|
|
This function is invoked if the cursor is pointing to a
|
|
CHECK-[IF, ELIF, ELSE, FI] line.
|
|
"""
|
|
variant = statement.variant
|
|
if variant is TestStatement.Variant.IF:
|
|
self._if(statement, variables)
|
|
elif variant is TestStatement.Variant.ELIF:
|
|
self._elif(statement, variables)
|
|
elif variant is TestStatement.Variant.ELSE:
|
|
self._else(statement)
|
|
else:
|
|
assert variant is TestStatement.Variant.FI
|
|
self._fi(statement)
|
|
|
|
def eof(self):
|
|
"""
|
|
The last line the cursor points to is always EOF.
|
|
"""
|
|
if not self._is_empty():
|
|
raise BadStructureException("Missing CHECK-FI", -1)
|
|
|
|
def _is_empty(self):
|
|
return not self.stack
|
|
|
|
def _if(self, statement, variables):
|
|
if not self._is_empty() and abs(self._peek()) in [IfStack.BRANCH_NOT_TAKEN,
|
|
IfStack.BRANCH_NOT_TAKEN_YET]:
|
|
self._push(IfStack.BRANCH_NOT_TAKEN)
|
|
elif evaluate_line(statement, variables):
|
|
self._push(IfStack.BRANCH_TAKEN)
|
|
else:
|
|
self._push(IfStack.BRANCH_NOT_TAKEN_YET)
|
|
|
|
def _elif(self, statement, variables):
|
|
if self._is_empty():
|
|
raise BadStructureException("CHECK-ELIF must be after CHECK-IF or CHECK-ELIF",
|
|
statement.line_no)
|
|
if self._peek() < 0:
|
|
raise BadStructureException("CHECK-ELIF cannot be after CHECK-ELSE", statement.line_no)
|
|
if self._peek() == IfStack.BRANCH_TAKEN:
|
|
self._set_last(IfStack.BRANCH_NOT_TAKEN)
|
|
elif self._peek() == IfStack.BRANCH_NOT_TAKEN_YET:
|
|
if evaluate_line(statement, variables):
|
|
self._set_last(IfStack.BRANCH_TAKEN)
|
|
# else, the CHECK-ELIF condition is False, so do nothing: the last element on the stack is
|
|
# already set to BRANCH_NOT_TAKEN_YET.
|
|
else:
|
|
assert self._peek() == IfStack.BRANCH_NOT_TAKEN
|
|
|
|
def _else(self, statement):
|
|
if self._is_empty():
|
|
raise BadStructureException("CHECK-ELSE must be after CHECK-IF or CHECK-ELIF",
|
|
statement.line_no)
|
|
if self._peek() < 0:
|
|
raise BadStructureException("Consecutive CHECK-ELSE statements", statement.line_no)
|
|
if self._peek() in [IfStack.BRANCH_TAKEN, IfStack.BRANCH_NOT_TAKEN]:
|
|
# Notice that we're setting -BRANCH_NOT_TAKEN rather that BRANCH_NOT_TAKEN as we went past the
|
|
# ELSE branch.
|
|
self._set_last(-IfStack.BRANCH_NOT_TAKEN)
|
|
else:
|
|
assert self._peek() == IfStack.BRANCH_NOT_TAKEN_YET
|
|
# Setting -BRANCH_TAKEN rather BRANCH_TAKEN for the same reason.
|
|
self._set_last(-IfStack.BRANCH_TAKEN)
|
|
|
|
def _fi(self, statement):
|
|
if self._is_empty():
|
|
raise BadStructureException("CHECK-FI does not have a matching CHECK-IF", statement.line_no)
|
|
self.stack.pop()
|
|
|
|
def _peek(self):
|
|
assert not self._is_empty()
|
|
return self.stack[-1]
|
|
|
|
def _push(self, element):
|
|
self.stack.append(element)
|
|
|
|
def _set_last(self, element):
|
|
self.stack[-1] = element
|
|
|
|
|
|
def find_matching_line(statement, c1_pass, scope, variables, exclude_lines=[]):
|
|
""" Finds the first line in `c1_pass` which matches `statement`.
|
|
|
|
Scan only lines numbered between `scope.start` and `scope.end` and not on the
|
|
`excludeLines` list.
|
|
|
|
Returns the index of the `c1Pass` line matching the statement and variables
|
|
values after the match.
|
|
|
|
Raises MatchFailedException if no such `c1Pass` line can be found.
|
|
"""
|
|
for i in range(scope.start, scope.end):
|
|
if i in exclude_lines:
|
|
continue
|
|
new_variables = match_lines(statement, c1_pass.body[i], variables)
|
|
if new_variables is not None:
|
|
return MatchInfo(MatchScope(i, i), new_variables)
|
|
raise MatchFailedException(statement, scope.start, variables)
|
|
|
|
|
|
class ExecutionState(object):
|
|
def __init__(self, c1_pass, variables={}):
|
|
self.cursor = 0
|
|
self.c1_pass = c1_pass
|
|
self.c1_length = len(c1_pass.body)
|
|
self.variables = ImmutableDict(variables)
|
|
self.dag_queue = []
|
|
self.not_queue = []
|
|
self.if_stack = IfStack()
|
|
self.last_variant = None
|
|
|
|
def move_cursor(self, match):
|
|
assert self.cursor <= match.scope.end
|
|
|
|
# Handle any pending NOT statements before moving the cursor
|
|
self.handle_not_queue(MatchScope(self.cursor, match.scope.start))
|
|
|
|
self.cursor = match.scope.end + 1
|
|
self.variables = match.variables
|
|
|
|
def handle_dag_queue(self, scope):
|
|
""" Attempts to find matching `c1Pass` lines for a group of DAG statements.
|
|
|
|
Statements are matched in the list order and variable values propagated. Only
|
|
lines in `scope` are scanned and each line can only match one statement.
|
|
|
|
Returns the range of `c1Pass` lines covered by this group (min/max of matching
|
|
line numbers) and the variable values after the match of the last statement.
|
|
|
|
Raises MatchFailedException when a statement cannot be satisfied.
|
|
"""
|
|
if not self.dag_queue:
|
|
return
|
|
|
|
matched_lines = []
|
|
variables = self.variables
|
|
|
|
for statement in self.dag_queue:
|
|
assert statement.variant == TestStatement.Variant.DAG
|
|
match = find_matching_line(statement, self.c1_pass, scope, variables, matched_lines)
|
|
variables = match.variables
|
|
assert match.scope.start == match.scope.end
|
|
assert match.scope.start not in matched_lines
|
|
matched_lines.append(match.scope.start)
|
|
|
|
match = MatchInfo(MatchScope(min(matched_lines), max(matched_lines)), variables)
|
|
self.dag_queue = []
|
|
self.move_cursor(match)
|
|
|
|
def handle_not_queue(self, scope):
|
|
""" Verifies that none of the given NOT statements matches a line inside
|
|
the given `scope` of `c1Pass` lines.
|
|
|
|
Raises MatchFailedException if a statement matches a line in the scope.
|
|
"""
|
|
for statement in self.not_queue:
|
|
assert statement.variant == TestStatement.Variant.NOT
|
|
for i in range(scope.start, scope.end):
|
|
if match_lines(statement, self.c1_pass.body[i], self.variables) is not None:
|
|
raise MatchFailedException(statement, i, self.variables)
|
|
self.not_queue = []
|
|
|
|
def handle_eof(self):
|
|
""" EOF marker always moves the cursor to the end of the file."""
|
|
match = MatchInfo(MatchScope(self.c1_length, self.c1_length), None)
|
|
self.move_cursor(match)
|
|
|
|
def handle_in_order(self, statement):
|
|
""" Single in-order statement. Find the first line that matches and move
|
|
the cursor to the subsequent line.
|
|
|
|
Raises MatchFailedException if no such line can be found.
|
|
"""
|
|
scope = MatchScope(self.cursor, self.c1_length)
|
|
match = find_matching_line(statement, self.c1_pass, scope, self.variables)
|
|
self.move_cursor(match)
|
|
|
|
def handle_next_line(self, statement):
|
|
""" Single next-line statement. Test if the current line matches and move
|
|
the cursor to the next line if it does.
|
|
|
|
Raises MatchFailedException if the current line does not match.
|
|
"""
|
|
if self.last_variant not in [TestStatement.Variant.IN_ORDER, TestStatement.Variant.NEXT_LINE]:
|
|
raise BadStructureException("A next-line statement can only be placed "
|
|
"after an in-order statement or another next-line statement.",
|
|
statement.line_no)
|
|
|
|
scope = MatchScope(self.cursor, self.cursor + 1)
|
|
match = find_matching_line(statement, self.c1_pass, scope, self.variables)
|
|
self.move_cursor(match)
|
|
|
|
def handle_eval(self, statement):
|
|
""" Evaluates the statement in the current context.
|
|
|
|
Raises MatchFailedException if the expression evaluates to False.
|
|
"""
|
|
if not evaluate_line(statement, self.variables):
|
|
raise MatchFailedException(statement, self.cursor, self.variables)
|
|
|
|
def handle(self, statement):
|
|
variant = None if statement is None else statement.variant
|
|
|
|
if variant in [TestStatement.Variant.IF,
|
|
TestStatement.Variant.ELIF,
|
|
TestStatement.Variant.ELSE,
|
|
TestStatement.Variant.FI]:
|
|
self.if_stack.handle(statement, self.variables)
|
|
return
|
|
|
|
if variant is None:
|
|
self.if_stack.eof()
|
|
|
|
if not self.if_stack.can_execute():
|
|
return
|
|
|
|
# First non-DAG statement always triggers execution of any preceding
|
|
# DAG statements.
|
|
if variant is not TestStatement.Variant.DAG:
|
|
self.handle_dag_queue(MatchScope(self.cursor, self.c1_length))
|
|
|
|
if variant is None:
|
|
self.handle_eof()
|
|
elif variant is TestStatement.Variant.IN_ORDER:
|
|
self.handle_in_order(statement)
|
|
elif variant is TestStatement.Variant.NEXT_LINE:
|
|
self.handle_next_line(statement)
|
|
elif variant is TestStatement.Variant.DAG:
|
|
self.dag_queue.append(statement)
|
|
elif variant is TestStatement.Variant.NOT:
|
|
self.not_queue.append(statement)
|
|
else:
|
|
assert variant is TestStatement.Variant.EVAL
|
|
self.handle_eval(statement)
|
|
|
|
self.last_variant = variant
|
|
|
|
|
|
def match_test_case(test_case, c1_pass, instruction_set_features):
|
|
""" Runs a test case against a C1visualizer graph dump.
|
|
|
|
Raises MatchFailedException when a statement cannot be satisfied.
|
|
"""
|
|
assert test_case.name == c1_pass.name
|
|
|
|
initial_variables = {"ISA_FEATURES": instruction_set_features}
|
|
state = ExecutionState(c1_pass, initial_variables)
|
|
test_statements = test_case.statements + [None]
|
|
for statement in test_statements:
|
|
state.handle(statement)
|
|
|
|
|
|
def match_files(checker_file, c1_file, target_arch, debuggable_mode, print_cfg):
|
|
for test_case in checker_file.test_cases:
|
|
if test_case.test_arch not in [None, target_arch]:
|
|
continue
|
|
if test_case.for_debuggable != debuggable_mode:
|
|
continue
|
|
|
|
# TODO: Currently does not handle multiple occurrences of the same group
|
|
# name, e.g. when a pass is run multiple times. It will always try to
|
|
# match a check group against the first output group of the same name.
|
|
c1_pass = c1_file.find_pass(test_case.name)
|
|
if c1_pass is None:
|
|
with open(c1_file.full_file_name) as cfg_file:
|
|
Logger.log("".join(cfg_file), Logger.Level.ERROR)
|
|
Logger.fail("Test case not found in the CFG file",
|
|
c1_file.full_file_name, test_case.start_line_no, test_case.name)
|
|
|
|
Logger.start_test(test_case.name)
|
|
try:
|
|
match_test_case(test_case, c1_pass, c1_file.instruction_set_features)
|
|
Logger.test_passed()
|
|
except MatchFailedException as e:
|
|
line_no = c1_pass.start_line_no + e.line_no
|
|
if e.statement.variant == TestStatement.Variant.NOT:
|
|
msg = "NOT statement matched line {}"
|
|
else:
|
|
msg = "Statement could not be matched starting from line {}"
|
|
msg = msg.format(line_no)
|
|
if print_cfg:
|
|
with open(c1_file.full_file_name) as cfg_file:
|
|
Logger.log("".join(cfg_file), Logger.Level.ERROR)
|
|
Logger.test_failed(msg, e.statement, e.variables)
|