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.
461 lines
15 KiB
461 lines
15 KiB
# 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 json
|
|
import re
|
|
import os
|
|
import hashlib
|
|
import collections
|
|
import itertools
|
|
|
|
from test_generator import data_types
|
|
from test_generator import generator
|
|
|
|
class DataTypeBuilder(object):
|
|
"""
|
|
Factory object for building `data_types.Operand` and `data_types.Input`
|
|
objects. This object stores information about all operand and input types
|
|
described in JSON as dictionnaries indexed by their identifier. See
|
|
`test/a32/config/data-types.json` as a reference.
|
|
|
|
Attributes:
|
|
operand_types Dictionnary of type names corresponding to the JSON
|
|
"type" field.
|
|
operand_variants Dictionnary of (variants, default) tuples.
|
|
|
|
input_types Dictionnary of type names corresponding to the JSON
|
|
"type" field.
|
|
input_values Dictionnary of (values, default) tuples.
|
|
"""
|
|
|
|
def __init__(self, operand_types, operand_variants, input_types,
|
|
input_values):
|
|
self.operand_types = operand_types
|
|
self.operand_variants = operand_variants
|
|
self.input_types = input_types
|
|
self.input_values = input_values
|
|
|
|
def BuildOperand(self, name, identifier):
|
|
"""
|
|
Build a `data_types.Operand` object with the name `name`. `identifier`
|
|
identifies which type we want to create, as declared in JSON.
|
|
"""
|
|
type_name = self.operand_types[identifier]
|
|
variants, default = self.operand_variants[identifier]
|
|
# We simply pass the `type_name` as a parameter which will be used verbatim
|
|
# in the code.
|
|
return data_types.Operand(name, type_name, variants, default)
|
|
|
|
def BuildInput(self, name, identifier):
|
|
"""
|
|
Build a `data_types.Input` object with the name `name`. `identifier`
|
|
identifies which type we want to create, as declared in JSON.
|
|
"""
|
|
type_name = self.input_types[identifier]
|
|
values, default = self.input_values[identifier]
|
|
# For `data_types.Input` types, the `type_name` refers to the actual name of
|
|
# the Python class, inheriting from `Input`. This is done so that different
|
|
# input types can generate different C++ code by overriding the `Load` and
|
|
# `Store` methods.
|
|
input_constructor = getattr(data_types, type_name)
|
|
return input_constructor(name, values, default)
|
|
|
|
|
|
def LoadJSON(filename):
|
|
"""
|
|
Read `filename`, strip its comments and load as JSON.
|
|
"""
|
|
with open(filename, "r") as f:
|
|
match_cpp_comments = re.compile("//.*\n")
|
|
# The order in which structures are described in JSON matters as we use them
|
|
# as a seed. Computing a hash from a unordered dict always gives a different
|
|
# value. We use the `object_pairs_hook` to make the json module create
|
|
# `OrderedDict` objects instead of builtin `dict` objects.
|
|
return json.loads(match_cpp_comments.sub("", f.read()),
|
|
object_pairs_hook=collections.OrderedDict)
|
|
|
|
|
|
def ParseDataTypes(json_data_types):
|
|
"""
|
|
Build a `DataTypeBuilder` object containing all information from the JSON
|
|
description in `json_data_types`.
|
|
|
|
~~~
|
|
{
|
|
"operands": [
|
|
{
|
|
"identifier": "AllRegistersButPC"
|
|
"type": "Register"
|
|
"variants": [
|
|
"r0",
|
|
"r1",
|
|
"r2",
|
|
"r3"
|
|
]
|
|
"default": "r0"
|
|
},
|
|
{
|
|
...
|
|
}
|
|
],
|
|
"inputs": [
|
|
{
|
|
"identifier": "Register"
|
|
"type": "Register"
|
|
"values": [
|
|
"0x00000000",
|
|
"0xffffffff",
|
|
"0xabababab"
|
|
]
|
|
"default": "0xabababab"
|
|
},
|
|
{
|
|
...
|
|
}
|
|
]
|
|
}
|
|
~~~
|
|
"""
|
|
operand_types = {
|
|
json_operand_type["identifier"]: json_operand_type["type"]
|
|
for json_operand_type in json_data_types["operands"]
|
|
}
|
|
operand_variants = {
|
|
json_operand_type["identifier"]:
|
|
(json_operand_type["variants"], json_operand_type["default"])
|
|
for json_operand_type in json_data_types["operands"]
|
|
}
|
|
input_types = {
|
|
json_input_type["identifier"]: json_input_type["type"]
|
|
for json_input_type in json_data_types["inputs"]
|
|
}
|
|
input_values = {
|
|
json_input_type["identifier"]:
|
|
(json_input_type["values"], json_input_type["default"])
|
|
for json_input_type in json_data_types["inputs"]
|
|
}
|
|
return DataTypeBuilder(operand_types, operand_variants, input_types, input_values)
|
|
|
|
|
|
def ParseDescription(data_type_builder, json_description):
|
|
"""
|
|
Parse the instruction description into a
|
|
(`generator.OperandList`, `generator.InputList`) tuple.
|
|
|
|
Example for an instruction that takes a condidition code, two registers and an
|
|
immediate as operand. It will also need inputs for the registers, as well as
|
|
NZCV flags.
|
|
~~~
|
|
{
|
|
"operands": [
|
|
{
|
|
"name": "cond",
|
|
"type": "Condition",
|
|
},
|
|
{
|
|
"name": "rd",
|
|
"type": "RegisterScratch",
|
|
},
|
|
{
|
|
"name": "rn",
|
|
"type": "RegisterScratch",
|
|
},
|
|
// The last operand needs to be wrapped into a C++ `Operand` object. We
|
|
// declare the operands that need to be wrapped as a list.
|
|
{
|
|
"name": "op",
|
|
"wrapper": "Operand",
|
|
"operands": [
|
|
{
|
|
"name": "immediate",
|
|
"type": "ModifiedImmediate",
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"inputs": [
|
|
{
|
|
"name": "apsr",
|
|
"type": "NZCV"
|
|
},
|
|
{
|
|
"name": "rd",
|
|
"type": "Register"
|
|
},
|
|
{
|
|
"name": "rn",
|
|
"type": "Register"
|
|
}
|
|
]
|
|
]
|
|
~~~
|
|
"""
|
|
|
|
operands = []
|
|
for json_operand in json_description["operands"]:
|
|
if "name" in json_operand and "type" in json_operand:
|
|
operands.append(data_type_builder.BuildOperand(json_operand["name"],
|
|
json_operand["type"]))
|
|
elif "name" in json_operand and \
|
|
"wrapper" in json_operand and \
|
|
"operands" in json_operand:
|
|
wrapped_operands = [
|
|
data_type_builder.BuildOperand(json_wrapped_operand["name"],
|
|
json_wrapped_operand["type"])
|
|
for json_wrapped_operand in json_operand["operands"]
|
|
]
|
|
operands.append(data_types.OperandWrapper(json_operand["name"],
|
|
json_operand["wrapper"],
|
|
wrapped_operands))
|
|
else:
|
|
raise Exception("Parser failed to recognize JSON \"description\".")
|
|
operand_list = generator.OperandList(operands)
|
|
|
|
json_description_inputs = json_description["inputs"]
|
|
input_list = generator.InputList([
|
|
data_type_builder.BuildInput(json_input["name"], json_input["type"])
|
|
for json_input in json_description_inputs
|
|
])
|
|
|
|
return operand_list, input_list
|
|
|
|
|
|
def ParseTestCase(json_test_case):
|
|
"""
|
|
Build a `generator.TestCase` object from its JSON description.
|
|
|
|
~~~
|
|
{
|
|
"name": "RdIsNotRn",
|
|
"operands": [
|
|
"rd", "rn"
|
|
],
|
|
"inputs": [
|
|
"rd", "rn"
|
|
],
|
|
"operand-filter": "rd != rn", // Python code to limit operand generation.
|
|
"operand-limit": 10 // Choose a random sample of 10 operands.
|
|
}
|
|
...
|
|
{
|
|
"name": "Flags",
|
|
"operands": [
|
|
"cond"
|
|
],
|
|
"inputs": [
|
|
"apsr", "q"
|
|
],
|
|
"input-filter": "q == \"QFlag\"", // Python code to limit input generation
|
|
"input-limit": 200 // Choose a random sample of 200 inputs.
|
|
}
|
|
...
|
|
{
|
|
"name": "InITBlock",
|
|
"operands": [
|
|
"cond", "rd", "rn", "rm"
|
|
],
|
|
"in-it-block": "{cond}", // Generate an extra IT instruction. This string
|
|
// will be used as the operand passed to IT. One
|
|
// needs to specify under what name the condition
|
|
// operand is represented, in braces.
|
|
"operand-filter": "cond != 'al' and rd == rm"
|
|
}
|
|
~~~
|
|
"""
|
|
|
|
# TODO: The fields in "operands" and "inputs" respectively refer to operands
|
|
# and inputs declared in the instruction description (see `ParseDescription`).
|
|
# We should assert that the user hasn't miss typed them and raise an
|
|
# exception.
|
|
|
|
# If the fields are not present, give them default values (empty list,
|
|
# "True", or "None").
|
|
operand_names = json_test_case["operands"] \
|
|
if "operands" in json_test_case else []
|
|
input_names = json_test_case["inputs"] if "inputs" in json_test_case else []
|
|
operand_filter = json_test_case["operand-filter"] \
|
|
if "operand-filter" in json_test_case else "True"
|
|
input_filter = json_test_case["input-filter"] \
|
|
if "input-filter" in json_test_case else "True"
|
|
operand_limit = json_test_case["operand-limit"] \
|
|
if "operand-limit" in json_test_case else None
|
|
input_limit = json_test_case["input-limit"] \
|
|
if "input-limit" in json_test_case else None
|
|
in_it_block = json_test_case["in-it-block"] \
|
|
if "in-it-block" in json_test_case else None
|
|
|
|
# Create a seed from the test case description. It will only change if the
|
|
# test case has changed.
|
|
md5 = hashlib.md5(str(json_test_case).encode())
|
|
seed = md5.hexdigest()
|
|
|
|
return generator.TestCase(json_test_case["name"], seed, operand_names, input_names,
|
|
operand_filter, input_filter, operand_limit,
|
|
input_limit, in_it_block)
|
|
|
|
|
|
def ParseTestFile(test_name, test_isa, mnemonics, operand_list, input_list,
|
|
json_test_file):
|
|
"""
|
|
Build a `generator.Generator` object from a test file description. We have one
|
|
for each generated test files.
|
|
|
|
~~~
|
|
{
|
|
"type": "simulator", // Type of the test. This will control the prefix we
|
|
// use when naming the file to generate.
|
|
"name": "special-case", // Optional name that will be included in the
|
|
// generated filename.
|
|
"mnemonics": [ // Optional list of instruction, overriding the top-level
|
|
"Adc", // one.
|
|
"Add",
|
|
...
|
|
],
|
|
"test-cases": [
|
|
... // Test case descriptions parsed with `ParseTestCase`.
|
|
]
|
|
}
|
|
~~~
|
|
"""
|
|
name = json_test_file["name"] if "name" in json_test_file else ""
|
|
if name is not "":
|
|
test_name = test_name + "-" + name
|
|
# Override the top-level mnemonics list with a subset.
|
|
if "mnemonics" in json_test_file:
|
|
if set(json_test_file["mnemonics"]) == set(mnemonics):
|
|
raise Exception(
|
|
"Overriding mnemonic list is identical to the top-level list")
|
|
if not(set(json_test_file["mnemonics"]) < set(mnemonics)):
|
|
raise Exception(
|
|
"Overriding mnemonic list should a subset of the top-level list")
|
|
mnemonics = json_test_file["mnemonics"]
|
|
test_cases = [
|
|
ParseTestCase(json_test_case)
|
|
for json_test_case in json_test_file["test-cases"]
|
|
]
|
|
return generator.Generator(test_name, test_isa, json_test_file["type"],
|
|
mnemonics, operand_list, input_list, test_cases)
|
|
|
|
|
|
def ParseConfig(test_name, test_isas, data_type_builder, json_config):
|
|
"""
|
|
Return a list of `generator.Generator` objects from a JSON description. This
|
|
is the top-level description.
|
|
|
|
~~~
|
|
{
|
|
"mnemonics": [
|
|
"Adc",
|
|
"Add",
|
|
...
|
|
],
|
|
"description": [
|
|
... // Instruction description parsed with `ParseDescription`.
|
|
],
|
|
"test-files": [
|
|
... // Test files descriptions parsed with `ParseTestFile`.
|
|
]
|
|
}
|
|
~~~
|
|
"""
|
|
mnemonics = json_config["mnemonics"]
|
|
operand_list, input_list = ParseDescription(
|
|
data_type_builder, json_config["description"])
|
|
|
|
return itertools.chain(*[[
|
|
ParseTestFile(test_name, test_isa, mnemonics, operand_list,
|
|
input_list, json_test_file)
|
|
for json_test_file in json_config["test-files"]
|
|
]
|
|
for test_isa in test_isas
|
|
])
|
|
|
|
|
|
def GetTestNameAndISAFromFileName(filename):
|
|
"""
|
|
Return a tuple (name, [isa, ...]) extracted from the file name.
|
|
"""
|
|
# Strip the ".json" extension
|
|
stripped_basename = os.path.splitext(os.path.basename(filename))[0]
|
|
# The ISA is the last element in the filename, seperated with "-".
|
|
if stripped_basename.endswith(('-a32', '-t32')):
|
|
isa = [stripped_basename[-3:]]
|
|
test_name = stripped_basename[:-4]
|
|
else:
|
|
# If the ISA is ommitted, support both.
|
|
isa = ["a32", "t32"]
|
|
test_name = stripped_basename
|
|
|
|
return (test_name, isa)
|
|
|
|
|
|
def GetTestNameFromFileName(filename):
|
|
"""
|
|
Return the name given to this test from its file name, stripped of the
|
|
optional "a32" or "t32" at the end.
|
|
"""
|
|
test_name, _ = GetTestNameAndISAFromFileName(filename)
|
|
return test_name
|
|
|
|
|
|
def GetISAsFromFileName(filename):
|
|
"""
|
|
Return a list of ISAs supported by the test, from the file name, either
|
|
["a32"], ["t32"] or both.
|
|
"""
|
|
_, isas = GetTestNameAndISAFromFileName(filename)
|
|
|
|
return isas
|
|
|
|
def Parse(data_type_file, config_files):
|
|
"""
|
|
Parse the `data_type_file` and `test_case_files` json description files into a
|
|
list of (name, test_case) tuples. Test cases are `generator.TestCase`
|
|
objects that can be used to generate C++.
|
|
"""
|
|
|
|
# Create a `DataTypeBuilder` object. This object will passed down and used to
|
|
# instantiate `data_types.Operand` and `data_types.Input` objects.
|
|
data_type_builder = ParseDataTypes(LoadJSON(data_type_file))
|
|
|
|
# Build a list of (name, JSON) tuples to represent the new tests.
|
|
json_configs = [
|
|
# We create the name of the test by looking at the file name stripped of
|
|
# its extension.
|
|
(GetTestNameFromFileName(config_file), GetISAsFromFileName(config_file),
|
|
LoadJSON(config_file))
|
|
for config_file in config_files
|
|
]
|
|
|
|
# Return a list of Generator objects. The generator is the entry point to
|
|
# generate a file.
|
|
# Note that `ParseConfig` returns a list of generators already. We use `chain`
|
|
# here to flatten a list of lists into just a list.
|
|
return itertools.chain(*[
|
|
ParseConfig(test_name, test_isas, data_type_builder, json_config)
|
|
for test_name, test_isas, json_config in json_configs
|
|
])
|