Pierre Langlois | 88c46b8 | 2016-06-02 18:15:32 +0100 | [diff] [blame^] | 1 | # Copyright 2016, ARM Limited |
| 2 | # All rights reserved. |
| 3 | # |
| 4 | # Redistribution and use in source and binary forms, with or without |
| 5 | # modification, are permitted provided that the following conditions are met: |
| 6 | # |
| 7 | # * Redistributions of source code must retain the above copyright notice, |
| 8 | # this list of conditions and the following disclaimer. |
| 9 | # * Redistributions in binary form must reproduce the above copyright notice, |
| 10 | # this list of conditions and the following disclaimer in the documentation |
| 11 | # and/or other materials provided with the distribution. |
| 12 | # * Neither the name of ARM Limited nor the names of its contributors may be |
| 13 | # used to endorse or promote products derived from this software without |
| 14 | # specific prior written permission. |
| 15 | # |
| 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS CONTRIBUTORS "AS IS" AND |
| 17 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| 18 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE |
| 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 26 | |
| 27 | import json |
| 28 | import re |
| 29 | import os |
| 30 | import hashlib |
| 31 | import collections |
| 32 | import itertools |
| 33 | |
| 34 | from test_generator import data_types |
| 35 | from test_generator import generator |
| 36 | |
| 37 | class DataTypeBuilder(object): |
| 38 | """ |
| 39 | Factory object for building `data_types.Operand` and `data_types.Input` |
| 40 | objects. This object stores information about all operand and input types |
| 41 | described in JSON as dictionnaries indexed by their identifier. See |
| 42 | `test/a32/config/data-types.json` as a reference. |
| 43 | |
| 44 | Attributes: |
| 45 | operand_types Dictionnary of type names corresponding to the JSON |
| 46 | "type" field. |
| 47 | operand_variants Dictionnary of (variants, default) tuples. |
| 48 | |
| 49 | input_types Dictionnary of type names corresponding to the JSON |
| 50 | "type" field. |
| 51 | input_values Dictionnary of (values, default) tuples. |
| 52 | """ |
| 53 | |
| 54 | def __init__(self, operand_types, operand_variants, input_types, |
| 55 | input_values): |
| 56 | self.operand_types = operand_types |
| 57 | self.operand_variants = operand_variants |
| 58 | self.input_types = input_types |
| 59 | self.input_values = input_values |
| 60 | |
| 61 | def BuildOperand(self, name, identifier): |
| 62 | """ |
| 63 | Build a `data_types.Operand` object with the name `name`. `identifier` |
| 64 | identifies which type we want to create, as declared in JSON. |
| 65 | """ |
| 66 | type_name = self.operand_types[identifier] |
| 67 | variants, default = self.operand_variants[identifier] |
| 68 | # We simply pass the `type_name` as a parameter which will be used verbatim |
| 69 | # in the code. |
| 70 | return data_types.Operand(name, type_name, variants, default) |
| 71 | |
| 72 | def BuildInput(self, name, identifier): |
| 73 | """ |
| 74 | Build a `data_types.Input` object with the name `name`. `identifier` |
| 75 | identifies which type we want to create, as declared in JSON. |
| 76 | """ |
| 77 | type_name = self.input_types[identifier] |
| 78 | values, default = self.input_values[identifier] |
| 79 | # For `data_types.Input` types, the `type_name` refers to the actual name of |
| 80 | # the Python class, inheriting from `Input`. This is done so that different |
| 81 | # input types can generate different C++ code by overriding the `Load` and |
| 82 | # `Store` methods. |
| 83 | input_constructor = getattr(data_types, type_name) |
| 84 | return input_constructor(name, values, default) |
| 85 | |
| 86 | |
| 87 | def LoadJSON(filename): |
| 88 | """ |
| 89 | Read `filename`, strip its comments and load as JSON. |
| 90 | """ |
| 91 | with open(filename, "r") as f: |
| 92 | match_cpp_comments = re.compile("//.*\n") |
| 93 | # The order in which structures are described in JSON matters as we use them |
| 94 | # as a seed. Computing a hash from a unordered dict always gives a different |
| 95 | # value. We use the `object_pairs_hook` to make the json module create |
| 96 | # `OrderedDict` objects instead of builtin `dict` objects. |
| 97 | return json.loads(match_cpp_comments.sub("", f.read()), |
| 98 | object_pairs_hook=collections.OrderedDict) |
| 99 | |
| 100 | |
| 101 | def ParseDataTypes(json_data_types): |
| 102 | """ |
| 103 | Build a `DataTypeBuilder` object containing all information from the JSON |
| 104 | description in `json_data_types`. |
| 105 | |
| 106 | ~~~ |
| 107 | { |
| 108 | "operands": [ |
| 109 | { |
| 110 | "identifier": "AllRegistersButPC" |
| 111 | "type": "Register" |
| 112 | "variants": [ |
| 113 | "r0", |
| 114 | "r1", |
| 115 | "r2", |
| 116 | "r3" |
| 117 | ] |
| 118 | "default": "r0" |
| 119 | }, |
| 120 | { |
| 121 | ... |
| 122 | } |
| 123 | ], |
| 124 | "inputs": [ |
| 125 | { |
| 126 | "identifier": "Register" |
| 127 | "type": "Register" |
| 128 | "values": [ |
| 129 | "0x00000000", |
| 130 | "0xffffffff", |
| 131 | "0xabababab" |
| 132 | ] |
| 133 | "default": "0xabababab" |
| 134 | }, |
| 135 | { |
| 136 | ... |
| 137 | } |
| 138 | ] |
| 139 | } |
| 140 | ~~~ |
| 141 | """ |
| 142 | operand_types = { |
| 143 | json_operand_type["identifier"]: json_operand_type["type"] |
| 144 | for json_operand_type in json_data_types["operands"] |
| 145 | } |
| 146 | operand_variants = { |
| 147 | json_operand_type["identifier"]: |
| 148 | (json_operand_type["variants"], json_operand_type["default"]) |
| 149 | for json_operand_type in json_data_types["operands"] |
| 150 | } |
| 151 | input_types = { |
| 152 | json_input_type["identifier"]: json_input_type["type"] |
| 153 | for json_input_type in json_data_types["inputs"] |
| 154 | } |
| 155 | input_values = { |
| 156 | json_input_type["identifier"]: |
| 157 | (json_input_type["values"], json_input_type["default"]) |
| 158 | for json_input_type in json_data_types["inputs"] |
| 159 | } |
| 160 | return DataTypeBuilder(operand_types, operand_variants, input_types, input_values) |
| 161 | |
| 162 | |
| 163 | def ParseDescription(data_type_builder, json_description): |
| 164 | """ |
| 165 | Parse the instruction description into a |
| 166 | (`generator.OperandList`, `generator.InputList`) tuple. |
| 167 | |
| 168 | Example for an instruction that takes a condidition code, two registers and an |
| 169 | immediate as operand. It will also need inputs for the registers, as well as |
| 170 | NZCV flags. |
| 171 | ~~~ |
| 172 | { |
| 173 | "operands": [ |
| 174 | { |
| 175 | "name": "cond", |
| 176 | "type": "Condition", |
| 177 | }, |
| 178 | { |
| 179 | "name": "rd", |
| 180 | "type": "RegisterScratch", |
| 181 | }, |
| 182 | { |
| 183 | "name": "rn", |
| 184 | "type": "RegisterScratch", |
| 185 | }, |
| 186 | // The last operand needs to be wrapped into a C++ `Operand` object. We |
| 187 | // declare the operands that need to be wrapped as a list. |
| 188 | { |
| 189 | "name": "op", |
| 190 | "wrapper": "Operand", |
| 191 | "operands": [ |
| 192 | { |
| 193 | "name": "immediate", |
| 194 | "type": "ModifiedImmediate", |
| 195 | } |
| 196 | ] |
| 197 | } |
| 198 | ], |
| 199 | "inputs": [ |
| 200 | { |
| 201 | "name": "apsr", |
| 202 | "type": "NZCV" |
| 203 | }, |
| 204 | { |
| 205 | "name": "rd", |
| 206 | "type": "Register" |
| 207 | }, |
| 208 | { |
| 209 | "name": "rn", |
| 210 | "type": "Register" |
| 211 | } |
| 212 | ] |
| 213 | ] |
| 214 | ~~~ |
| 215 | """ |
| 216 | |
| 217 | operands = [] |
| 218 | for json_operand in json_description["operands"]: |
| 219 | if "name" in json_operand and "type" in json_operand: |
| 220 | operands.append(data_type_builder.BuildOperand(json_operand["name"], |
| 221 | json_operand["type"])) |
| 222 | elif "name" in json_operand and \ |
| 223 | "wrapper" in json_operand and \ |
| 224 | "operands" in json_operand: |
| 225 | wrapped_operands = [ |
| 226 | data_type_builder.BuildOperand(json_wrapped_operand["name"], |
| 227 | json_wrapped_operand["type"]) |
| 228 | for json_wrapped_operand in json_operand["operands"] |
| 229 | ] |
| 230 | operands.append(data_types.OperandWrapper(json_operand["name"], |
| 231 | json_operand["wrapper"], |
| 232 | wrapped_operands)) |
| 233 | else: |
| 234 | raise Exception("Parser failed to recognize JSON \"description\".") |
| 235 | operand_list = generator.OperandList(operands) |
| 236 | |
| 237 | json_description_inputs = json_description["inputs"] |
| 238 | input_list = generator.InputList([ |
| 239 | data_type_builder.BuildInput(json_input["name"], json_input["type"]) |
| 240 | for json_input in json_description_inputs |
| 241 | ]) |
| 242 | |
| 243 | return operand_list, input_list |
| 244 | |
| 245 | |
| 246 | def ParseTestCase(json_test_case): |
| 247 | """ |
| 248 | Build a `generator.TestCase` object from its JSON description. |
| 249 | |
| 250 | ~~~ |
| 251 | { |
| 252 | "name": "RdIsNotRn", |
| 253 | "operands": [ |
| 254 | "rd", "rn" |
| 255 | ], |
| 256 | "inputs": [ |
| 257 | "rd", "rn" |
| 258 | ], |
| 259 | "operand-filter": "rd != rn", // Python code to limit operand generation. |
| 260 | "operand-limit": 10 // Choose a random sample of 10 operands. |
| 261 | } |
| 262 | ... |
| 263 | { |
| 264 | "name": "Flags", |
| 265 | "operands": [ |
| 266 | "cond" |
| 267 | ], |
| 268 | "inputs": [ |
| 269 | "apsr", "q" |
| 270 | ], |
| 271 | "input-filter": "q == \"QFlag\"", // Python code to limit input generation |
| 272 | "input-limit": 200 // Choose a random sample of 200 inputs. |
| 273 | } |
| 274 | ... |
| 275 | { |
| 276 | "name": "InITBlock", |
| 277 | "operands": [ |
| 278 | "cond", "rd", "rn", "rm" |
| 279 | ], |
| 280 | // Make sure the macro-assembler has generated an IT instruction. |
| 281 | "expect-instruction-before": "It {cond}", |
| 282 | "operand-filter": "cond != 'al' and rd == rm" |
| 283 | } |
| 284 | ~~~ |
| 285 | """ |
| 286 | |
| 287 | # TODO: The fields in "operands" and "inputs" respectively refer to operands |
| 288 | # and inputs declared in the instruction description (see `ParseDescription`). |
| 289 | # We should assert that the user hasn't miss typed them and raise an |
| 290 | # exception. |
| 291 | |
| 292 | # If the fields are not present, give them default values (empty list, |
| 293 | # "True", or "None"). |
| 294 | operand_names = json_test_case["operands"] \ |
| 295 | if "operands" in json_test_case else [] |
| 296 | input_names = json_test_case["inputs"] if "inputs" in json_test_case else [] |
| 297 | operand_filter = json_test_case["operand-filter"] \ |
| 298 | if "operand-filter" in json_test_case else "True" |
| 299 | input_filter = json_test_case["input-filter"] \ |
| 300 | if "input-filter" in json_test_case else "True" |
| 301 | operand_limit = json_test_case["operand-limit"] \ |
| 302 | if "operand-limit" in json_test_case else None |
| 303 | input_limit = json_test_case["input-limit"] \ |
| 304 | if "input-limit" in json_test_case else None |
| 305 | expect_instruction_before = json_test_case["expect-instruction-before"] + ";" \ |
| 306 | if "expect-instruction-before" in json_test_case else "" |
| 307 | |
| 308 | # Create a seed from the test case description. It will only change if the |
| 309 | # test case has changed. |
| 310 | md5 = hashlib.md5(str(json_test_case).encode()) |
| 311 | seed = md5.hexdigest() |
| 312 | |
| 313 | return generator.TestCase(json_test_case["name"], seed, operand_names, input_names, |
| 314 | operand_filter, input_filter, operand_limit, |
| 315 | input_limit, expect_instruction_before) |
| 316 | |
| 317 | |
| 318 | def ParseTestFile(test_name, test_isa, mnemonics, operand_list, input_list, |
| 319 | json_test_file): |
| 320 | """ |
| 321 | Build a `generator.Generator` object from a test file description. We have one |
| 322 | for each generated test files. |
| 323 | |
| 324 | ~~~ |
| 325 | { |
| 326 | "type": "simulator", // Type of the test. This will control the prefix we |
| 327 | // use when naming the file to generate. |
| 328 | "name": "special-case", // Optional name that will be included in the |
| 329 | // generated filename. |
| 330 | "mnemonics": [ // Optional list of instruction, overriding the top-level |
| 331 | "Adc", // one. |
| 332 | "Add", |
| 333 | ... |
| 334 | ], |
| 335 | "test-cases": [ |
| 336 | ... // Test case descriptions parsed with `ParseTestCase`. |
| 337 | ] |
| 338 | } |
| 339 | ~~~ |
| 340 | """ |
| 341 | name = json_test_file["name"] if "name" in json_test_file else "" |
| 342 | if name is not "": |
| 343 | test_name = test_name + "-" + name |
| 344 | # Override the top-level mnemonics list with a subset. |
| 345 | if "mnemonics" in json_test_file: |
| 346 | if set(json_test_file["mnemonics"]) == set(mnemonics): |
| 347 | raise Exception( |
| 348 | "Overriding mnemonic list is identical to the top-level list") |
| 349 | if not(set(json_test_file["mnemonics"]) < set(mnemonics)): |
| 350 | raise Exception( |
| 351 | "Overriding mnemonic list should a subset of the top-level list") |
| 352 | mnemonics = json_test_file["mnemonics"] |
| 353 | test_cases = [ |
| 354 | ParseTestCase(json_test_case) |
| 355 | for json_test_case in json_test_file["test-cases"] |
| 356 | ] |
| 357 | return generator.Generator(test_name, test_isa, json_test_file["type"], |
| 358 | mnemonics, operand_list, input_list, test_cases) |
| 359 | |
| 360 | |
| 361 | def ParseConfig(test_name, test_isa, data_type_builder, json_config): |
| 362 | """ |
| 363 | Return a list of `generator.Generator` objects from a JSON description. This |
| 364 | is the top-level description. |
| 365 | |
| 366 | ~~~ |
| 367 | { |
| 368 | "mnemonics": [ |
| 369 | "Adc", |
| 370 | "Add", |
| 371 | ... |
| 372 | ], |
| 373 | "description": [ |
| 374 | ... // Instruction description parsed with `ParseDescription`. |
| 375 | ], |
| 376 | "test-files": [ |
| 377 | ... // Test files descriptions parsed with `ParseTestFile`. |
| 378 | ] |
| 379 | } |
| 380 | ~~~ |
| 381 | """ |
| 382 | mnemonics = json_config["mnemonics"] |
| 383 | operand_list, input_list = ParseDescription( |
| 384 | data_type_builder, json_config["description"]) |
| 385 | |
| 386 | return [ |
| 387 | ParseTestFile(test_name, test_isa, mnemonics, operand_list, input_list, |
| 388 | json_test_file) |
| 389 | for json_test_file in json_config["test-files"] |
| 390 | ] |
| 391 | |
| 392 | |
| 393 | def GetTestNameFromFileName(filename): |
| 394 | """ |
| 395 | Return the name given to this test from its file name. |
| 396 | """ |
| 397 | return os.path.splitext(os.path.basename(filename))[0] |
| 398 | |
| 399 | |
| 400 | def GetISAFromFileName(filename): |
| 401 | """ |
| 402 | Return the ISA from the file name, either "a32" or "t32". |
| 403 | """ |
| 404 | isa = GetTestNameFromFileName(filename).split("-")[-1] |
| 405 | assert(isa in ["a32", "t32"]) |
| 406 | return isa |
| 407 | |
| 408 | |
| 409 | def Parse(data_type_file, config_files): |
| 410 | """ |
| 411 | Parse the `data_type_file` and `test_case_files` json description files into a |
| 412 | list of (name, test_case) tuples. Test cases are `generator.TestCase` |
| 413 | objects that can be used to generate C++. |
| 414 | """ |
| 415 | |
| 416 | # Create a `DataTypeBuilder` object. This object will passed down and used to |
| 417 | # instantiate `data_types.Operand` and `data_types.Input` objects. |
| 418 | data_type_builder = ParseDataTypes(LoadJSON(data_type_file)) |
| 419 | |
| 420 | # Build a list of (name, JSON) tuples to represent the new tests. |
| 421 | json_configs = [ |
| 422 | # We create the name of the test by looking at the file name stripped of |
| 423 | # its extension. |
| 424 | (GetTestNameFromFileName(config_file), GetISAFromFileName(config_file), |
| 425 | LoadJSON(config_file)) |
| 426 | for config_file in config_files |
| 427 | ] |
| 428 | |
| 429 | # Return a list of Generator objects. The generator is the entry point to |
| 430 | # generate a file. |
| 431 | # Note that `ParseConfig` returns a list of generators already. We use `chain` |
| 432 | # here to flatten a list of lists into just a list. |
| 433 | return itertools.chain(*[ |
| 434 | ParseConfig(test_name, test_isa, data_type_builder, json_config) |
| 435 | for test_name, test_isa, json_config in json_configs |
| 436 | ]) |