# Copyright 2016, VIXL authors # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # * Neither the name of ARM Limited nor the names of its contributors may be # used to endorse or promote products derived from this software without # specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import itertools import random import os.path from copy import deepcopy class OperandList(object): """ Convenience class representing a list of operand objects. It can be viewed is an iterator over operand objects. Attributes: operand_list """ def __init__(self, operand_list): self.operand_list = operand_list def __iter__(self): return iter(self.operand_list) def unwrap(self): """ Return a list of `Operand` objects, unwrapping `OperandWrapper` objects into `Operand` objects. For example: ~~~ Condition, Register, Operand(Register, Shift, Register) ~~~ Unwraps to: ~~~ Condition, Register, Register, Shift, Register ~~~ """ return itertools.chain(*self.operand_list) def ExcludeVariants(self, type_name, variant_to_exclude): """ Remove variants in `variant_to_exclude` from operands with type `type_name`. """ # Find the list of operand with type `type_name`. relevant_operands = filter(lambda operand: operand.type_name == type_name, self) for operand in relevant_operands: # Remove the intersection of the existing variants and variants we do not # want. for variant in set(operand.variants) & set(variant_to_exclude): operand.variants.remove(variant) def GetNames(self): """ Return the list of all `Operand` names, excluding `OperandWrapper` objects. """ return [operand.name for operand in self.unwrap()] class InputList(object): """ Convevience class representing a list of input objects. This class is an iterator over input objects. Attributes: inputs """ def __init__(self, inputs): self.inputs = inputs def __iter__(self): return iter(self.inputs) def GetNames(self): """ Return the list of input names. """ return [input.name for input in self] class TestCase(object): """ Object representation of a test case, as described in JSON. This object is used to build sets of operands and inputs that will be used by the generator to produce C++ arrays. Attributes: name Name of the test case, it is used to name the array to produce. seed Seed value to use for reproducable random generation. operand_names List of operand names this test case covers. input_names List of input names this test case covers. operand_filter Python expression as a string to filter out operands. input_filter Python expression as a string to filter out inputs. operand_limit Optional limit of the number of operands to generate. input_limit Optional limit of the number of inputs to generate. it_condition If not None, an IT instruction needs to be generated for the instruction under test to be valid. This member is a string template indicating the name of the condition operand, to be used with "format". For example, it will most likely have the value "{cond}". """ # Declare functions that will be callable from Python expressions in # `self.operand_filter`. operand_filter_runtime = { 'register_is_low': lambda register: register in ["r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7"] } def __init__(self, name, seed, operand_names, input_names, operand_filter, input_filter, operand_limit, input_limit, it_condition): self.name = name self.seed = seed self.operand_names = operand_names self.input_names = input_names self.operand_filter = operand_filter self.input_filter = input_filter self.operand_limit = operand_limit self.input_limit = input_limit self.it_condition = it_condition def GenerateOperands(self, operand_types): """ Generate a list of tuples, each tuple describing what operands to pass to an instruction to encode it. We use this to generate operand definitions. The algorithm used is a simple product of all operand variants. To limit what we generate, we choose to apply the product only on operands with their name in the `self.operand_names` list. Additionally, we use the Python expression in `self.operand_filter` to filter out tuples we do not want. Argument: operand_types The `OperandList` object that describe the form of the instruction to generate code for. """ # Build a list of all possible variants as a list of tuples. If the # operand's name is not in `self.operand_names`, then we restrict the list # to contain default variant. Each tuple in the list has the form # `(name, [variant1, variant2, ...])`. For example: # # [ # ('cond', ['al', 'ne', 'eq', ...]), # All condition variants. # ('rd', ['r0', 'r1', ...]), # All register variants. # ('rn', ['r0']) # Default register variant (r0). # ... # ] variants = [ [(operand_type.name, variant) for variant in operand_type.variants] if operand_type.name in self.operand_names else [(operand_type.name, operand_type.default)] for operand_type in operand_types.unwrap() ] lambda_string = "lambda {args}: {expression}".format( args=",".join(operand_types.GetNames()), expression=self.operand_filter) filter_lambda = eval(lambda_string, self.operand_filter_runtime) def BuildOperandDefinition(operands): """ Take a list of tuples describing the operands and build a definition from it. A definition is a tuple with a list of variants and a `expect_instruction_before` string. For example, we are turning this: [ ('cond', 'ne'), ('rd', 'r0'), ('rn', 'r1'), ('rm', 'r0) [ Into: (['ne', 'r0', 'r1', 'r0'], "It ne;") """ return ( # Build a list of operands by only keeping the second element of each # tuple. [operand[1] for operand in operands], # The next field is a boolean indicating if the test case needs to # generate an IT instruction. "true" if self.it_condition else "false", # If so, what condition should it be? self.it_condition.format(**dict(operands)) if self.it_condition else "al" ) # Build and return a list of operand definitions by computing the product of # all variants and filtering them with `filter_lambda`. # # Operand definitions consist of a list with a list of variants and an # optional `expect_instruction_before` string. For example: # # [ # (['al', 'r0', 'r1', 'r2'], ""), # (['ne', 'r0', 'r1', 'r0'], "It ne;"), # ... # ] # # Here, the filtered product of variants builds a list of lists of tuples, as such: # # [ # [('cond', 'al'), ('rd', 'r0'), ('rn', 'r1'), ('rn', 'r2')] # [('cond', 'ne'), ('rd', 'r0'), ('rn', 'r1'), ('rn', 'r0')], # ... # ] # # We then pass them to `BuildOperandDefinition` to produce the expected form # out of it. result = [ BuildOperandDefinition(operands) for operands in itertools.product(*variants) if filter_lambda(**dict(operands)) ] if self.operand_limit is None: return result else: # Use a fixed seed to randomly choose a limited set of operands. random.seed(self.seed) return random.sample(result, self.operand_limit) def GenerateInputs(self, input_types): """ Generate a list of tuples, each tuple describing what input to pass to an instruction at runtime. We use this to generate input definitions. The algorithm used is a simple product of all input values. To limit what we generate, we choose to apply the product only on inputs with their name in the `self.input_names` list. Additionally, we use the Python expression in `self.input_filter` to filter out tuples we do not want. Argument: input_types The `InputList` object describing the list of inputs the instruction can take. """ # Build a list of all possible values as a list of lists. If the input's # name is not in `self.input_names`, then we restrict the list to the # default value. values = [ input_type.values if input_type.name in self.input_names else [input_type.default] for input_type in input_types ] lambda_string = "lambda {args}: {expression}".format( args=", ".join(input_types.GetNames()), expression=self.input_filter) filter_lambda = eval(lambda_string) # Build and return a list of input definitions, such as # [('NoFlag', '0xffffffff', 0xabababab'), ...] for example. result = [ input_definition for input_definition in itertools.product(*values) if filter_lambda(*input_definition) ] if self.input_limit is None: return result else: # Use a fixed seed to randomly choose a limited set of inputs. random.seed(self.seed) return random.sample(result, self.input_limit) class Generator(object): """ A `Generator` object contains all information needed to generate a test file. Each method will return a string used to fill a variable in a template. Attributes: test_name Name of the test inferred from the name of the configuration file. It has the following form: `type-op1-op2-op3-isa`. test_type Type of the test, extracted from test_name. mnemonics List of instruction mnemonics. operands `OperandList` object. inputs `InputList` object. test_cases List of `TestCase` objects. """ def __init__(self, test_name, test_isa, test_type, mnemonics, operands, inputs, test_cases): self.test_name = test_name self.test_isa = test_isa self.test_type = test_type self.mnemonics = mnemonics self.inputs = inputs self.test_cases = test_cases # A simulator test cannot easily make use of the PC and SP registers. if self.test_type == "simulator": # We need to explicitely create our own deep copy the operands before we # can modify them. self.operands = deepcopy(operands) self.operands.ExcludeVariants("Register", ["r13", "r15"]) else: self.operands = operands def MnemonicToMethodName(self, mnemonic): if self.test_type in ["simulator", "macro-assembler"]: # Return a MacroAssembler method name return mnemonic.capitalize() else: # Return an Assembler method name method_name = mnemonic.lower() return "and_" if method_name == "and" else method_name def InstructionListDeclaration(self): """ ~~~ M(Adc) \ M(Adcs) \ M(Add) \ M(Adds) \ M(And) \ ... ~~~ """ return "".join([ "M({}) \\\n".format(self.MnemonicToMethodName(mnemonic)) for mnemonic in self.mnemonics ]) def OperandDeclarations(self): """ ~~~ Condition cond; Register rd; Register rn; ... ~~~ """ return "".join([operand.Declare() for operand in self.operands]) def InputDeclarations(self): """ ~~~ uint32_t cond; uint32_t rd; uint32_t rn; ... ~~~ """ return "".join([input.Declare() for input in self.inputs]) def InputDefinitions(self): """ ~~~ static const Inputs kCondition[] = {{...},{...}, ...}; static const Inputs kRdIsRd[] = {{...},{...}, ...}; ... ~~~ """ def InputDefinition(test_input): inputs = [ "{{{}}}".format(",".join(input)) for input in test_input.GenerateInputs(self.inputs) ] return """static const Inputs k{name}[] = {{ {input} }}; """.format(name=test_input.name, input=",".join(inputs)) return "\n".join(map(InputDefinition, self.test_cases)) def TestCaseDefinitions(self): """ For simulator tests: ~~~ {{eq, r0, r0, ...}, "eq r0 r0 ...", "Condition_eq_r0_...", ARRAY_SIZE(kCondition), kCondition}, ... {{eq, r0, r0, ...}, "eq r0 r0 ...", "RdIsRd_eq_r0_...", ARRAY_SIZE(kRdIsRd), kRdIsRn}, ... ~~~ For assembler tests: ~~~ {{eq, r0, r0, ...}, "", "eq r0 r0 ...", "Condition_eq_r0_...", ... {{eq, r0, r0, ...}, "", "eq r0 r0 ...", "RdIsRd_eq_r0_..."} ... {{eq, r0, r0, ...}, "It eq", "eq r0 r0 ...", "RdIsRd_eq_r0_..."} ... ~~~ """ def SimulatorTestCaseDefinition(test_case): test_cases = [ """{{ {{ {operands} }}, "{operands_description}", "{identifier}", ARRAY_SIZE(k{test_case_name}), k{test_case_name} }} """.format(operands=",".join(operand), operands_description=" ".join(operand), identifier=test_case.name + "_" + "_".join(operand), test_case_name=test_case.name) for operand, _, _ in test_case.GenerateOperands(self.operands) ] return ",\n".join(test_cases) def AssemblerTestCaseDefinition(test_case): test_cases = [ """{{ {{ {operands} }}, {in_it_block}, {it_condition}, "{operands_description}", "{identifier}" }} """.format(operands=",".join(operand), in_it_block=in_it_block, it_condition=it_condition, operands_description=" ".join(operand), identifier="_".join(operand)) for operand, in_it_block, it_condition in test_case.GenerateOperands(self.operands) ] return ",\n".join(test_cases) def MacroAssemblerTestCaseDefinition(test_case): test_cases = [ """{{ {{ {operands} }}, "{operands_description}", "{identifier}" }} """.format(operands=",".join(operand), operands_description=", ".join(operand), identifier="_".join(operand)) for operand, _, _ in test_case.GenerateOperands(self.operands) ] return ",\n".join(test_cases) if self.test_type == "simulator": return ",\n".join(map(SimulatorTestCaseDefinition, self.test_cases)) elif self.test_type == "assembler": return ",\n".join(map(AssemblerTestCaseDefinition, self.test_cases)) elif self.test_type == "macro-assembler": return ",\n".join(map(MacroAssemblerTestCaseDefinition, self.test_cases)) else: raise Exception("Unrecognized test type \"{}\".".format(self.test_type)) def IncludeTraceFiles(self): """ ~~~ #include "aarch32/traces/sim-...-a32.h" #include "aarch32/traces/sim-...-a32.h" ... ~~~ """ operands = "-".join(self.operands.GetNames()) return "".join([ "#include \"aarch32/traces/" + self.GetTraceFileName(mnemonic) + "\"\n" for mnemonic in self.mnemonics ]) def MacroAssemblerMethodArgs(self): """ ~~~ Condition cond, Register rd, Register rm, const Operand& immediate ~~~ """ return ", ".join([ operand.GetArgumentType() + " " + operand.name for operand in self.operands ]) def MacroAssemblerSetISA(self): """ Generate code to set the ISA. """ if self.test_isa == "t32": return "masm.UseT32();" else: return "masm.UseA32();" def CodeInstantiateOperands(self): """ ~~~ Condition cond = kTests[i].operands.cond; Register rd = kTests[i].operands.rd; ... ~~~ """ code = "".join([operand.Instantiate() for operand in self.operands]) if self.test_type in ["simulator", "macro-assembler"]: # Simulator tests need scratch registers to function and uses # `UseScratchRegisterScope` to dynamically allocate them. We need to # exclude all register operands from the list of available scratch # registers. # MacroAssembler test also need to ensure that they don't try to run tests # with registers that are scratch registers; the MacroAssembler contains # assertions to protect against such usage. excluded_registers = [ "scratch_registers.Exclude({});".format(operand.name) for operand in self.operands.unwrap() if operand.type_name == "Register" ] return code + "\n".join(excluded_registers) return code def CodePrologue(self): """ ~~~ __ Ldr(rn, MemOperand(input_ptr, offsetof(Inputs, rn))); __ Ldr(rm, MemOperand(input_ptr, offsetof(Inputs, rm))); ... ~~~ """ return "".join([input.Prologue() for input in self.inputs]) def CodeEpilogue(self): """ ~~~ __ Str(rn, MemOperand(result_ptr, offsetof(Inputs, rn))); __ Str(rm, MemOperand(result_ptr, offsetof(Inputs, rm))); ... ~~~ """ return "".join([input.Epilogue() for input in self.inputs]) def CodeParameterList(self): """ ~~~ cond, rd, rn, immediate ~~~ """ return ", ".join([ operand.name for operand in self.operands ]) def TracePrintOutputs(self): """ ~~~ printf("0x%08" PRIx32, results[i]->outputs[j].cond); printf(", "); printf("0x%08" PRIx32, results[i]->outputs[j].rd); printf(", "); ... ~~~ """ return "printf(\", \");".join( [input.PrintOutput() for input in self.inputs]) def CheckInstantiateResults(self): """ ~~~ uint32_t cond = results[i]->outputs[j].cond; uint32_t rd = results[i]->outputs[j].rd; ... ~~~ """ return "".join([input.InstantiateResult() for input in self.inputs]) def CheckInstantiateInputs(self): """ ~~~ uint32_t cond_input = kTests[i].inputs[j].cond; uint32_t rd_input = kTests[i].inputs[j].rd; ... ~~~ """ return "".join([input.InstantiateInput("_input") for input in self.inputs]) def CheckInstantiateReferences(self): """ ~~~ uint32_t cond_ref = reference[i].outputs[j].cond; uint32_t rd_ref = reference[i].outputs[j].rd; ... ~~~ """ return "".join([input.InstantiateReference("_ref") for input in self.inputs]) def CheckResultsAgainstReferences(self): """ ~~~ (cond != cond_ref) || (rd != rd_ref) || ... ~~~ """ return " || ".join([input.Compare("", "!=", "_ref") for input in self.inputs]) def CheckPrintInput(self): """ ~~~ printf("0x%08" PRIx32, cond_input); printf(", "); printf("0x%08" PRIx32, rd_input); printf(", "); ... ~~~ """ return "printf(\", \");".join( [input.PrintInput("_input") for input in self.inputs]) def CheckPrintExpected(self): """ ~~~ printf("0x%08" PRIx32, cond_ref); printf(", "); printf("0x%08" PRIx32, rd_ref); printf(", "); ... ~~~ """ return "printf(\", \");".join( [input.PrintInput("_ref") for input in self.inputs]) def CheckPrintFound(self): """ ~~~ printf("0x%08" PRIx32, cond); printf(", "); printf("0x%08" PRIx32, rd); printf(", "); ... ~~~ """ return "printf(\", \");".join( [input.PrintInput("") for input in self.inputs]) def TestName(self): """ ~~~ SIMULATOR_COND_RD_RN_RM_... ~~~ """ return self.test_type.replace("-", "_").upper() + "_" + \ self.test_name.replace("-", "_").upper() def GetTraceFileName(self, mnemonic): """ Return the name of a trace file for a given mnemonic. """ return self.test_type + "-" + self.test_name + "-" + \ mnemonic.lower() + ".h" def WriteEmptyTraces(self, output_directory): """ Write out empty trace files so we can compile the new test cases. """ for mnemonic in self.mnemonics: # The MacroAssembler tests have no traces. if self.test_type == "macro-assembler": continue with open(os.path.join(output_directory, self.GetTraceFileName(mnemonic)), "w") as f: code = "static const TestResult *kReference{} = NULL;\n" f.write(code.format(self.MnemonicToMethodName(mnemonic))) def GetIsaGuard(self): """ This guard ensure the ISA of the test is enabled. """ if 'A32' in self.TestName(): return 'VIXL_INCLUDE_TARGET_A32' else: assert 'T32' in self.TestName() return 'VIXL_INCLUDE_TARGET_T32'