1#!/usr/bin/python3 2# Copyright 2021 The ANGLE Project Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5# 6# gen_spirv_builder_and_parser.py: 7# Code generation for SPIR-V instruction builder and parser. 8# NOTE: don't run this script directly. Run scripts/run_code_generation.py. 9 10import itertools 11import json 12import os 13import sys 14 15# ANGLE uses instructions from SPIR-V 1.0 mostly, but also OpCopyLogical from SPIR-V 1.4. 16SPIRV_GRAMMAR_FILE = '../../../third_party/spirv-headers/src/include/spirv/unified1/spirv.core.grammar.json' 17 18# The script has two sets of outputs, a header and source file for SPIR-V code generation, and a 19# header and source file for SPIR-V parsing. 20SPIRV_BUILDER_FILE = 'spirv_instruction_builder' 21SPIRV_PARSER_FILE = 'spirv_instruction_parser' 22 23# The types are either defined in spirv_types.h (to use strong types), or are enums that are 24# defined by SPIR-V headers. 25ANGLE_DEFINED_TYPES = [ 26 'IdRef', 'IdResult', 'IdResultType', 'IdMemorySemantics', 'IdScope', 'LiteralInteger', 27 'LiteralString', 'LiteralContextDependentNumber', 'LiteralExtInstInteger', 28 'PairLiteralIntegerIdRef', 'PairIdRefLiteralInteger', 'PairIdRefIdRef' 29] 30 31HEADER_TEMPLATE = """// GENERATED FILE - DO NOT EDIT. 32// Generated by {script_name} using data from {data_source_name}. 33// 34// Copyright 2021 The ANGLE Project Authors. All rights reserved. 35// Use of this source code is governed by a BSD-style license that can be 36// found in the LICENSE file. 37// 38// {file_name}_autogen.h: 39// Functions to {verb} SPIR-V binary for each instruction. 40 41#ifndef COMMON_SPIRV_{file_name_capitalized}AUTOGEN_H_ 42#define COMMON_SPIRV_{file_name_capitalized}AUTOGEN_H_ 43 44#include <spirv/unified1/spirv.hpp> 45 46#include "spirv_types.h" 47 48namespace angle 49{{ 50namespace spirv 51{{ 52{prototype_list} 53}} // namespace spirv 54}} // namespace angle 55 56#endif // COMMON_SPIRV_{file_name_capitalized}AUTOGEN_H_ 57""" 58 59SOURCE_TEMPLATE = """// GENERATED FILE - DO NOT EDIT. 60// Generated by {script_name} using data from {data_source_name}. 61// 62// Copyright 2021 The ANGLE Project Authors. All rights reserved. 63// Use of this source code is governed by a BSD-style license that can be 64// found in the LICENSE file. 65// 66// {file_name}_autogen.cpp: 67// Functions to {verb} SPIR-V binary for each instruction. 68 69#include "{file_name}_autogen.h" 70 71#include <string.h> 72 73#include "common/debug.h" 74 75namespace angle 76{{ 77namespace spirv 78{{ 79{helper_functions} 80 81{function_list} 82}} // namespace spirv 83}} // namespace angle 84""" 85 86BUILDER_HELPER_FUNCTIONS = """namespace 87{ 88uint32_t MakeLengthOp(size_t length, spv::Op op) 89{ 90 ASSERT(length <= 0xFFFFu); 91 ASSERT(op <= 0xFFFFu); 92 93 // It's easy for a complex shader to be crafted to hit the length limit, 94 // turn that into a crash instead of a security bug. Ideally, the compiler 95 // would gracefully fail compilation, so this is more of a safety net. 96 if (ANGLE_UNLIKELY(length > 0xFFFFu)) 97 { 98 ERR() << "Complex shader not representible in SPIR-V"; 99 ANGLE_CRASH(); 100 } 101 102 return static_cast<uint32_t>(length) << 16 | op; 103} 104} // anonymous namespace 105 106void WriteSpirvHeader(std::vector<uint32_t> *blob, uint32_t version, uint32_t idCount) 107{ 108 // Header: 109 // 110 // - Magic number 111 // - Version (1.X) 112 // - ANGLE's Generator number: 113 // * 24 for tool id (higher 16 bits) 114 // * 1 for tool version (lower 16 bits)) 115 // - Bound (idCount) 116 // - 0 (reserved) 117 constexpr uint32_t kANGLEGeneratorId = 24; 118 constexpr uint32_t kANGLEGeneratorVersion = 1; 119 120 ASSERT(blob->empty()); 121 122 blob->push_back(spv::MagicNumber); 123 blob->push_back(version); 124 blob->push_back(kANGLEGeneratorId << 16 | kANGLEGeneratorVersion); 125 blob->push_back(idCount); 126 blob->push_back(0x00000000); 127} 128""" 129 130BUILDER_HELPER_FUNCTION_PROTOTYPE = """ 131 void WriteSpirvHeader(std::vector<uint32_t> *blob, uint32_t version, uint32_t idCount); 132""" 133 134PARSER_FIXED_FUNCTIONS_PROTOTYPES = """void GetInstructionOpAndLength(const uint32_t *_instruction, 135 spv::Op *opOut, uint32_t *lengthOut); 136""" 137 138PARSER_FIXED_FUNCTIONS = """void GetInstructionOpAndLength(const uint32_t *_instruction, 139 spv::Op *opOut, uint32_t *lengthOut) 140{ 141 constexpr uint32_t kOpMask = 0xFFFFu; 142 *opOut = static_cast<spv::Op>(_instruction[0] & kOpMask); 143 *lengthOut = _instruction[0] >> 16; 144} 145""" 146 147TEMPLATE_BUILDER_FUNCTION_PROTOTYPE = """void Write{op}(Blob *blob {param_list})""" 148TEMPLATE_BUILDER_FUNCTION_BODY = """{{ 149 const size_t startSize = blob->size(); 150 blob->push_back(0); 151 {push_back_lines} 152 (*blob)[startSize] = MakeLengthOp(blob->size() - startSize, spv::Op{op}); 153}} 154""" 155 156TEMPLATE_PARSER_FUNCTION_PROTOTYPE = """void Parse{op}(const uint32_t *_instruction {param_list})""" 157TEMPLATE_PARSER_FUNCTION_BODY = """{{ 158 spv::Op _op; 159 uint32_t _length; 160 GetInstructionOpAndLength(_instruction, &_op, &_length); 161 ASSERT(_op == spv::Op{op}); 162 uint32_t _o = 1; 163 {parse_lines} 164}} 165""" 166 167 168def load_grammar(grammar_file): 169 with open(grammar_file) as grammar_in: 170 grammar = json.loads(grammar_in.read()) 171 172 return grammar 173 174 175def remove_chars(string, chars): 176 return ''.join(list(filter(lambda c: c not in chars, string))) 177 178 179def make_camel_case(name): 180 return name[0].lower() + name[1:] 181 182 183class Writer: 184 185 def __init__(self): 186 self.path_prefix = os.path.dirname(os.path.realpath(__file__)) + os.path.sep 187 self.grammar = load_grammar(self.path_prefix + SPIRV_GRAMMAR_FILE) 188 189 # List of extensions needed by ANGLE 190 cherry_picked_extensions = {'SPV_EXT_fragment_shader_interlock'} 191 192 # If an instruction has a parameter of these types, the instruction is ignored 193 self.unsupported_kinds = set(['LiteralSpecConstantOpInteger']) 194 # If an instruction requires a capability of these kinds, the instruction is ignored 195 self.unsupported_capabilities = set(['Kernel', 'Addresses']) 196 # If an instruction requires an extension other than these, the instruction is ignored 197 self.supported_extensions = set([]) | cherry_picked_extensions 198 # List of bit masks. These have 'Mask' added to their typename in SPIR-V headers. 199 self.bit_mask_types = set([]) 200 201 # List of generated instructions builder/parser functions so far. 202 self.instruction_builder_prototypes = [BUILDER_HELPER_FUNCTION_PROTOTYPE] 203 self.instruction_builder_impl = [] 204 self.instruction_parser_prototypes = [PARSER_FIXED_FUNCTIONS_PROTOTYPES] 205 self.instruction_parser_impl = [PARSER_FIXED_FUNCTIONS] 206 207 def write_builder_and_parser(self): 208 """Generates four files, a set of header and source files for generating SPIR-V instructions 209 and a set for parsing SPIR-V instructions. Only Vulkan instructions are processed (and not 210 OpenCL for example), and some assumptions are made based on ANGLE's usage (for example that 211 constants always fit in one 32-bit unit, as GLES doesn't support double or 64-bit types). 212 213 Additionally, enums and other parameter 'kinds' are not parsed from the json file, but 214 rather use the definitions from the SPIR-V headers repository and the spirv_types.h file.""" 215 216 # Recurse through capabilities and accumulate ones that depend on unsupported ones. 217 self.accumulate_unsupported_capabilities() 218 219 self.find_bit_mask_types() 220 221 for instruction in self.grammar['instructions']: 222 self.generate_instruction_functions(instruction) 223 224 # Write out the files. 225 data_source_base_name = os.path.basename(SPIRV_GRAMMAR_FILE) 226 builder_template_args = { 227 'script_name': os.path.basename(sys.argv[0]), 228 'data_source_name': data_source_base_name, 229 'file_name': SPIRV_BUILDER_FILE, 230 'file_name_capitalized': remove_chars(SPIRV_BUILDER_FILE.upper(), '_'), 231 'verb': 'generate', 232 'helper_functions': BUILDER_HELPER_FUNCTIONS, 233 'prototype_list': ''.join(self.instruction_builder_prototypes), 234 'function_list': ''.join(self.instruction_builder_impl) 235 } 236 parser_template_args = { 237 'script_name': os.path.basename(sys.argv[0]), 238 'data_source_name': data_source_base_name, 239 'file_name': SPIRV_PARSER_FILE, 240 'file_name_capitalized': remove_chars(SPIRV_PARSER_FILE.upper(), '_'), 241 'verb': 'parse', 242 'helper_functions': '', 243 'prototype_list': ''.join(self.instruction_parser_prototypes), 244 'function_list': ''.join(self.instruction_parser_impl) 245 } 246 247 with (open(self.path_prefix + SPIRV_BUILDER_FILE + '_autogen.h', 'w')) as f: 248 f.write(HEADER_TEMPLATE.format(**builder_template_args)) 249 250 with (open(self.path_prefix + SPIRV_BUILDER_FILE + '_autogen.cpp', 'w')) as f: 251 f.write(SOURCE_TEMPLATE.format(**builder_template_args)) 252 253 with (open(self.path_prefix + SPIRV_PARSER_FILE + '_autogen.h', 'w')) as f: 254 f.write(HEADER_TEMPLATE.format(**parser_template_args)) 255 256 with (open(self.path_prefix + SPIRV_PARSER_FILE + '_autogen.cpp', 'w')) as f: 257 f.write(SOURCE_TEMPLATE.format(**parser_template_args)) 258 259 def requires_unsupported_capability(self, item): 260 depends = item.get('capabilities', []) 261 if len(depends) == 0: 262 return False 263 return all([dep in self.unsupported_capabilities for dep in depends]) 264 265 def requires_unsupported_extension(self, item): 266 extensions = item.get('extensions', []) 267 return any([ext not in self.supported_extensions for ext in extensions]) 268 269 def accumulate_unsupported_capabilities(self): 270 operand_kinds = self.grammar['operand_kinds'] 271 272 # Find the Capability enum 273 for kind in filter(lambda entry: entry['kind'] == 'Capability', operand_kinds): 274 capabilities = kind['enumerants'] 275 for capability in capabilities: 276 name = capability['enumerant'] 277 # For each capability, see if any of the capabilities they depend on is unsupported. 278 # If so, add the capability to the list of unsupported ones. 279 if self.requires_unsupported_capability(capability): 280 self.unsupported_capabilities.add(name) 281 continue 282 # Do the same for extensions 283 if self.requires_unsupported_extension(capability): 284 self.unsupported_capabilities.add(name) 285 286 def find_bit_mask_types(self): 287 operand_kinds = self.grammar['operand_kinds'] 288 289 # Find the BitEnum categories 290 for bitEnumEntry in filter(lambda entry: entry['category'] == 'BitEnum', operand_kinds): 291 self.bit_mask_types.add(bitEnumEntry['kind']) 292 293 def get_operand_name(self, operand, operand_distinguisher): 294 kind = operand['kind'] 295 name = operand.get('name') 296 297 # If no name is given, derive the name from the kind 298 if name is None: 299 assert (kind.find(' ') == -1) 300 return make_camel_case(kind) + str(operand_distinguisher) 301 302 quantifier = operand.get('quantifier', '') 303 name = remove_chars(name, "'") 304 305 # First, a number of special-cases for optional lists 306 if quantifier == '*': 307 suffix = 'List' 308 309 # For IdRefs, change 'Xyz 1', +\n'Xyz 2', +\n...' to xyzList 310 if kind == 'IdRef': 311 if name.find(' ') != -1: 312 name = name[0:name.find(' ')] 313 314 # Otherwise, if it's a pair in the form of 'Xyz, Abc, ...', change it to xyzAbcPairList 315 elif kind.startswith('Pair'): 316 suffix = 'PairList' 317 318 # Otherwise, it's just a list, so change `xyz abc` to `xyzAbcList 319 320 name = remove_chars(name, " ,.") 321 return make_camel_case(name) + suffix 322 323 # Otherwise, remove invalid characters and make the first letter lower case. 324 name = remove_chars(name, " .,+\n~") 325 326 name = make_camel_case(name) 327 328 # Make sure the name is not a C++ keyword 329 return 'default_' if name == 'default' else name 330 331 def get_operand_namespace(self, kind): 332 return '' if kind in ANGLE_DEFINED_TYPES else 'spv::' 333 334 def get_operand_type_suffix(self, kind): 335 return 'Mask' if kind in self.bit_mask_types else '' 336 337 def get_kind_cpp_type(self, kind): 338 return self.get_operand_namespace(kind) + kind + self.get_operand_type_suffix(kind) 339 340 def get_operand_type_in_and_out(self, operand): 341 kind = operand['kind'] 342 quantifier = operand.get('quantifier', '') 343 344 is_array = quantifier == '*' 345 is_optional = quantifier == '?' 346 cpp_type = self.get_kind_cpp_type(kind) 347 348 if is_array: 349 type_in = 'const ' + cpp_type + 'List &' 350 type_out = cpp_type + 'List *' 351 elif is_optional: 352 type_in = 'const ' + cpp_type + ' *' 353 type_out = cpp_type + ' *' 354 else: 355 type_in = cpp_type 356 type_out = cpp_type + ' *' 357 358 return (type_in, type_out, is_array, is_optional) 359 360 def get_operand_push_back_line(self, operand, operand_name, is_array, is_optional): 361 kind = operand['kind'] 362 pre = '' 363 post = '' 364 accessor = '.' 365 item = operand_name 366 item_dereferenced = item 367 if is_optional: 368 # If optional, surround with an if. 369 pre = 'if (' + operand_name + ') {\n' 370 post = '\n}' 371 accessor = '->' 372 item_dereferenced = '*' + item 373 elif is_array: 374 # If array, surround with a loop. 375 pre = 'for (const auto &operand : ' + operand_name + ') {\n' 376 post = '\n}' 377 item = 'operand' 378 item_dereferenced = item 379 380 # Usually the operand is one uint32_t, but it may be pair. Handle the pairs especially. 381 if kind == 'PairLiteralIntegerIdRef': 382 line = 'blob->push_back(' + item + accessor + 'literal);' 383 line += 'blob->push_back(' + item + accessor + 'id);' 384 elif kind == 'PairIdRefLiteralInteger': 385 line = 'blob->push_back(' + item + accessor + 'id);' 386 line += 'blob->push_back(' + item + accessor + 'literal);' 387 elif kind == 'PairIdRefIdRef': 388 line = 'blob->push_back(' + item + accessor + 'id1);' 389 line += 'blob->push_back(' + item + accessor + 'id2);' 390 elif kind == 'LiteralString': 391 line = '{size_t d = blob->size();' 392 line += 'blob->resize(d + strlen(' + item_dereferenced + ') / 4 + 1, 0);' 393 # We currently don't have any big-endian devices in the list of supported platforms. 394 # Literal strings in SPIR-V are stored little-endian (SPIR-V 1.0 Section 2.2.1, Literal 395 # String), so if a big-endian device is to be supported, the string copy here should 396 # be adjusted. 397 line += 'ASSERT(IsLittleEndian());' 398 line += 'strcpy(reinterpret_cast<char *>(blob->data() + d), ' + item_dereferenced + ');}' 399 else: 400 line = 'blob->push_back(' + item_dereferenced + ');' 401 402 return pre + line + post 403 404 def get_operand_parse_line(self, operand, operand_name, is_array, is_optional): 405 kind = operand['kind'] 406 pre = '' 407 post = '' 408 accessor = '->' 409 410 if is_optional: 411 # If optional, surround with an if, both checking if argument is provided, and whether 412 # it exists. 413 pre = 'if (' + operand_name + ' && _o < _length) {\n' 414 post = '\n}' 415 elif is_array: 416 # If array, surround with an if and a loop. 417 pre = 'if (' + operand_name + ') {\n' 418 pre += 'while (_o < _length) {\n' 419 post = '\n}}' 420 accessor = '.' 421 422 # Usually the operand is one uint32_t, but it may be pair. Handle the pairs especially. 423 if kind == 'PairLiteralIntegerIdRef': 424 if is_array: 425 line = operand_name + '->emplace_back(' + kind + '{LiteralInteger(_instruction[_o]), IdRef(_instruction[_o + 1])});' 426 line += '_o += 2;' 427 else: 428 line = operand_name + '->literal = LiteralInteger(_instruction[_o++]);' 429 line += operand_name + '->id = IdRef(_instruction[_o++]);' 430 elif kind == 'PairIdRefLiteralInteger': 431 if is_array: 432 line = operand_name + '->emplace_back(' + kind + '{IdRef(_instruction[_o]), LiteralInteger(_instruction[_o + 1])});' 433 line += '_o += 2;' 434 else: 435 line = operand_name + '->id = IdRef(_instruction[_o++]);' 436 line += operand_name + '->literal = LiteralInteger(_instruction[_o++]);' 437 elif kind == 'PairIdRefIdRef': 438 if is_array: 439 line = operand_name + '->emplace_back(' + kind + '{IdRef(_instruction[_o]), IdRef(_instruction[_o + 1])});' 440 line += '_o += 2;' 441 else: 442 line = operand_name + '->id1 = IdRef(_instruction[_o++]);' 443 line += operand_name + '->id2 = IdRef(_instruction[_o++]);' 444 elif kind == 'LiteralString': 445 # The length of string in words is ceil((strlen(str) + 1) / 4). This is equal to 446 # (strlen(str)+1+3) / 4, which is equal to strlen(str)/4+1. 447 assert (not is_array) 448 # See handling of LiteralString in get_operand_push_back_line. 449 line = 'ASSERT(IsLittleEndian());' 450 line += '*' + operand_name + ' = reinterpret_cast<const char *>(&_instruction[_o]);' 451 line += '_o += strlen(*' + operand_name + ') / 4 + 1;' 452 else: 453 if is_array: 454 line = operand_name + '->emplace_back(_instruction[_o++]);' 455 else: 456 line = '*' + operand_name + ' = ' + self.get_kind_cpp_type( 457 kind) + '(_instruction[_o++]);' 458 459 return pre + line + post 460 461 def process_operand(self, operand, operand_distinguisher, cpp_operands_in, cpp_operands_out, 462 cpp_in_parse_lines, cpp_out_push_back_lines): 463 operand_name = self.get_operand_name(operand, operand_distinguisher) 464 type_in, type_out, is_array, is_optional = self.get_operand_type_in_and_out(operand) 465 466 # Make the parameter list 467 cpp_operands_in.append(', ' + type_in + ' ' + operand_name) 468 cpp_operands_out.append(', ' + type_out + ' ' + operand_name) 469 470 # Make the builder body lines 471 cpp_out_push_back_lines.append( 472 self.get_operand_push_back_line(operand, operand_name, is_array, is_optional)) 473 474 # Make the parser body lines 475 cpp_in_parse_lines.append( 476 self.get_operand_parse_line(operand, operand_name, is_array, is_optional)) 477 478 def generate_instruction_functions(self, instruction): 479 name = instruction['opname'] 480 assert (name.startswith('Op')) 481 name = name[2:] 482 483 # Skip if the instruction depends on a capability or extension we aren't interested in 484 if self.requires_unsupported_capability(instruction): 485 return 486 if self.requires_unsupported_extension(instruction): 487 return 488 489 operands = instruction.get('operands', []) 490 491 # Skip if any of the operands are not supported 492 if any([operand['kind'] in self.unsupported_kinds for operand in operands]): 493 return 494 495 cpp_operands_in = [] 496 cpp_operands_out = [] 497 cpp_in_parse_lines = [] 498 cpp_out_push_back_lines = [] 499 500 operand_distinguisher = 0 501 for operand in operands: 502 operand_distinguisher += 1 503 504 self.process_operand(operand, operand_distinguisher, cpp_operands_in, cpp_operands_out, 505 cpp_in_parse_lines, cpp_out_push_back_lines) 506 507 # get_operand_parse_line relies on there only being one array parameter, and it being 508 # the last. 509 assert (operand.get('quantifier') != '*' or len(cpp_in_parse_lines) == len(operands)) 510 511 if operand['kind'] == 'Decoration': 512 # Special handling of Op*Decorate instructions with a Decoration operand. That 513 # operand always comes last, and implies a number of LiteralIntegers to follow. 514 assert (len(cpp_in_parse_lines) == len(operands)) 515 516 decoration_operands = { 517 'name': 'values', 518 'kind': 'LiteralInteger', 519 'quantifier': '*' 520 } 521 self.process_operand(decoration_operands, operand_distinguisher, cpp_operands_in, 522 cpp_operands_out, cpp_in_parse_lines, cpp_out_push_back_lines) 523 524 elif operand['kind'] == 'ExecutionMode': 525 # Special handling of OpExecutionMode instruction with an ExecutionMode operand. 526 # That operand always comes last, and implies a number of LiteralIntegers to follow. 527 assert (len(cpp_in_parse_lines) == len(operands)) 528 529 execution_mode_operands = { 530 'name': 'operands', 531 'kind': 'LiteralInteger', 532 'quantifier': '*' 533 } 534 self.process_operand(execution_mode_operands, operand_distinguisher, 535 cpp_operands_in, cpp_operands_out, cpp_in_parse_lines, 536 cpp_out_push_back_lines) 537 538 elif operand['kind'] == 'ImageOperands': 539 # Special handling of OpImage* instructions with an ImageOperands operand. That 540 # operand always comes last, and implies a number of IdRefs to follow with different 541 # meanings based on the bits set in said operand. 542 assert (len(cpp_in_parse_lines) == len(operands)) 543 544 image_operands = {'name': 'imageOperandIds', 'kind': 'IdRef', 'quantifier': '*'} 545 self.process_operand(image_operands, operand_distinguisher, cpp_operands_in, 546 cpp_operands_out, cpp_in_parse_lines, cpp_out_push_back_lines) 547 548 # Make the builder prototype body 549 builder_prototype = TEMPLATE_BUILDER_FUNCTION_PROTOTYPE.format( 550 op=name, param_list=''.join(cpp_operands_in)) 551 self.instruction_builder_prototypes.append(builder_prototype + ';\n') 552 self.instruction_builder_impl.append( 553 builder_prototype + '\n' + TEMPLATE_BUILDER_FUNCTION_BODY.format( 554 op=name, push_back_lines='\n'.join(cpp_out_push_back_lines))) 555 556 if len(operands) == 0: 557 return 558 559 parser_prototype = TEMPLATE_PARSER_FUNCTION_PROTOTYPE.format( 560 op=name, param_list=''.join(cpp_operands_out)) 561 self.instruction_parser_prototypes.append(parser_prototype + ';\n') 562 self.instruction_parser_impl.append( 563 parser_prototype + '\n' + TEMPLATE_PARSER_FUNCTION_BODY.format( 564 op=name, parse_lines='\n'.join(cpp_in_parse_lines))) 565 566 567def main(): 568 569 # auto_script parameters. 570 if len(sys.argv) > 1: 571 if sys.argv[1] == 'inputs': 572 print(SPIRV_GRAMMAR_FILE) 573 elif sys.argv[1] == 'outputs': 574 output_files_base = [SPIRV_BUILDER_FILE, SPIRV_PARSER_FILE] 575 output_files = [ 576 '_autogen.'.join(pair) 577 for pair in itertools.product(output_files_base, ['h', 'cpp']) 578 ] 579 print(','.join(output_files)) 580 else: 581 print('Invalid script parameters') 582 return 1 583 return 0 584 585 writer = Writer() 586 writer.write_builder_and_parser() 587 588 return 0 589 590 591if __name__ == '__main__': 592 sys.exit(main()) 593