Subversion Repositories Games.Descent

Rev

Blame | Last modification | View Log | Download | RSS feed

  1. #!/usr/bin/python
  2.  
  3. import ast, os, re, sys
  4.  
  5. # Storage for the relevant fields from an initializer for a single
  6. # element of a kc_item array.  Fields not required for sorting or
  7. # generating the output header are not captured or saved.
  8. class ArrayInitializationLine:
  9.         def __init__(self,xinput,y,name,enum,label,idx):
  10.                 # xinput, y are fields in the kc_item structure
  11.                 self.xinput = xinput
  12.                 self.y = y
  13.                 # name is the token that will be preprocessor-defined to the
  14.                 # appropriate udlr values
  15.                 self.name = name
  16.                 self.enum = enum
  17.                 self.label = label
  18.                 # index in the kc_item array at which this initializer was found
  19.                 self.idx = idx
  20.  
  21.         # For horizontal sorting, group elements by their vertical
  22.         # coordinate, then break ties using the horizontal coordinate.  This
  23.         # causes elements from a single row to be contiguous in the sorted
  24.         # output, and places visually adjacent rows adjacent in the sorted
  25.         # output.
  26.         def key_horizontal(self):
  27.                 return (self.y, self.xinput)
  28.  
  29.         # For vertical sorting, group elements by their horizontal
  30.         # coordinate, then break ties using the vertical coordinate.
  31.         def key_vertical(self):
  32.                 return (self.xinput, self.y)
  33.  
  34. # InputException is raised when the post-processed C++ table cannot be
  35. # parsed using the regular expressions in this script.  The table may or
  36. # may not be valid C++ when this script rejects it.
  37. class InputException(Exception):
  38.         pass
  39.  
  40. # NodeVisitor handles walking a Python Abstract Syntax Tree (AST) to
  41. # emulate a few supported operations, and raise an error on anything
  42. # unsupported.  This is used to implement primitive support for
  43. # evaluating arithmetic expressions in the table.  This support covers
  44. # only what is likely to be used and assumes Python syntax (which should
  45. # usually match C++ syntax closely enough, for the limited forms
  46. # supported).
  47. class NodeVisitor(ast.NodeVisitor):
  48.         def __init__(self,source,lno,name,expr,constants):
  49.                 self.source = source
  50.                 self.lno = lno
  51.                 self.name = name
  52.                 self.expr = expr
  53.                 self.constants = constants
  54.  
  55.         def generic_visit(self,node):
  56.                 raise InputException('%s:%u: %r expression %r uses unsupported node type %s' % (self.source, self.lno, self.name, self.expr, node.__class__.__name__))
  57.  
  58.         def visit_BinOp(self,node):
  59.                 left = self.visit(node.left)
  60.                 right = self.visit(node.right)
  61.                 op = node.op
  62.                 if isinstance(op, ast.Add):
  63.                         return left + right
  64.                 elif isinstance(op, ast.Sub):
  65.                         return left - right
  66.                 elif isinstance(op, ast.Mult):
  67.                         return left * right
  68.                 elif isinstance(op, ast.Div):
  69.                         return left / right
  70.                 else:
  71.                         raise InputException('%s:%u: %r expression %r uses unsupported BinOp node type %s' % (self.source, self.lno, self.name, self.expr, op.__class__.__name__))
  72.  
  73.         # Resolve expressions by expanding them.
  74.         def visit_Expression(self,node):
  75.                 return self.visit(node.body)
  76.  
  77.         # Resolve names by searching the dictionary `constants`, which is
  78.         # initialized from C++ constexpr declarations found while scanning
  79.         # the file.
  80.         def visit_Name(self,node):
  81.                 try:
  82.                         return self.constants[node.id]
  83.                 except KeyError as e:
  84.                         raise InputException('%s:%u: %r expression %r uses undefined name %s' % (self.source, self.lno, self.name, self.expr, node.id))
  85.  
  86.         # Resolve numbers by returning the value as-is.
  87.         @staticmethod
  88.         def visit_Num(node):
  89.                 return node.n
  90.  
  91. class Main:
  92.         def __init__(self):
  93.                 # Initially, no constants are known.  Elements will be added by
  94.                 # prepare_text.
  95.                 self.constants = {}
  96.  
  97.         # Resolve an expression by parsing it into an AST, then visiting
  98.         # the nodes and emulating the permitted operations.  Any
  99.         # disallowed operations will cause an exception, which will
  100.         # propagate through resolve_expr into the caller.
  101.         def resolve_expr(self,source,lno,name,expr):
  102.                 expr = expr.strip()
  103.                 return NodeVisitor(source, lno, name, expr, self.constants).visit(ast.parse(expr, mode='eval'))
  104.  
  105.         # Given a list of C++ initializer lines, extract from those lines
  106.         # the position of each initialized cell, compute the u/d/l/r
  107.         # relations among the cells, and return a multiline string
  108.         # containing CPP #define statements appropriate to the cells.  Each
  109.         # C++ initializer must be entirely contained within a single element
  110.         # in `lines`.
  111.         #
  112.         # array_name - C++ identifier for the array; used in a C
  113.         # comment and, if labels are used, as part of the name of the macro
  114.         # that expands to the label
  115.         #
  116.         # source - name of the file from which the lines were read
  117.         #
  118.         # lines - iterable of initializer lines
  119.         #
  120.         # _re_finditer_init_element - bound method for a regular expression to
  121.         # extract the required fields
  122.         def generate_defines(self,array_name,source,lines,_re_finditer_init_element=re.compile(r'{'
  123.                         r'(?:[^,]+),'   # x
  124.                         r'(?P<y>[^,]+),'
  125.                         r'(?P<xinput>[^,]+),'
  126.                         r'(?:[^,]+),'   # w2
  127.                         r'\s*(?:DXX_KCONFIG_UI_ENUM\s*\((?P<enum>\w+)\)\s*)?(?:DXX_KCONFIG_UI_LABEL\s*\((?P<label>(?:"[^"]*")|\w*)\)\s*)?(?P<udlr>\w+)\(\),'
  128.                         r'\s*(?:[^,]+,' # type
  129.                                 r'[^,]+,'       # state_bit
  130.                                 r'[^,]+)'       # state_ptr
  131.                         r'},').finditer):
  132.                 a = ArrayInitializationLine
  133.                 array = []
  134.                 append = array.append
  135.                 resolve_expr = self.resolve_expr
  136.                 idx = 0
  137.                 # Iterate over the initializer lines and populate `array` with
  138.                 # the extracted data.
  139.                 for lno, line in lines:
  140.                         ml = _re_finditer_init_element(line)
  141.                         old_idx = idx
  142.                         for m in ml:
  143.                                 m = m.group
  144.                                 append(a(
  145.                                         resolve_expr(source, lno, 'xinput', m('xinput')),
  146.                                         resolve_expr(source, lno, 'y', m('y')),
  147.                                         m('udlr'), m('enum'), m('label'), idx))
  148.                                 idx = idx + 1
  149.                         # If the loop executes zero times, then the regular
  150.                         # expression failed to match, `old_idx is idx` is True, and
  151.                         # an exception should be raised, because well-formed input
  152.                         # will always match at least once.
  153.                         #
  154.                         # If the loop executes at least once, then a result was
  155.                         # found, not `old_idx is idx`, and no exception is necessary.
  156.                         if old_idx is idx:
  157.                                 raise InputException('%s:%u: failed to match regex for line %r\n' % (source, lno, line))
  158.                 if not array:
  159.                         # An empty array is not useful, but may exist when the
  160.                         # developer is adding a new array and has not yet defined
  161.                         # any elements.
  162.                         return '\n/* %s - udlr blank */' % array_name
  163.                 # Generate a temporary list with the elements sorted for u/d
  164.                 # navigation.  Walk the temporary and assign appropriate u/d
  165.                 # references.
  166.                 s = sorted(array, key=a.key_vertical)
  167.                 p = s[-1]
  168.                 for i in s:
  169.                         i.next_u = p
  170.                         p.next_d = i
  171.                         p = i
  172.                 # As above, but sorted for l/r navigation.
  173.                 s = sorted(array, key=a.key_horizontal)
  174.                 p = s[-1]
  175.                 for i in s:
  176.                         i.next_l = p
  177.                         p.next_r = i
  178.                         p = i
  179.                 # This must be a `#define` since it expands to a comma-separated
  180.                 # list of values for a structure initializer.
  181.                 template_define_udlr = '#define {0}()\t/* [{1:2d}] */\t{2:2d},{3:3d},{4:3d},{5:3d}'.format
  182.                 # Use an `enum class` here so that the values can only be used
  183.                 # in arrays specifically marked for this index type.  This
  184.                 # prevents mixing types, such as indexing the mouse array with
  185.                 # joystick index values.
  186.                 #
  187.                 # Generate `#define` statements to allow the consuming code to
  188.                 # use `#ifdef` to detect which members exist.
  189.                 template_define_enum_header = 'enum class dxx_kconfig_ui_{0} : unsigned {{'.format
  190.                 template_define_enum_member = '''\
  191. #define dxx_kconfig_ui_{0}_{1} dxx_kconfig_ui_{0}::{1}
  192.         {1} = {2},'''.format
  193.                 define_enum_footer = '};'
  194.                 template_define_label_value_fragment = '\t\\\n\t/* [{1:2d}] */\t{0} "\\0"'.format
  195.                 enum = []
  196.                 label = []
  197.                 # Generate the `#define` lines using the relations computed by
  198.                 # the preceding loops.  Both ordering loops must execute
  199.                 # completely before the next_* data required by the udlr
  200.                 # `#define` lines is available.  The udlr logic cannot be folded
  201.                 # into a prior loop.
  202.                 #
  203.                 # The enum member and label logic could be handled in a prior
  204.                 # loop, but are more logical here.  Moving them to an earlier
  205.                 # loop offers no gain.
  206.                 result = ['/* {0} - udlr define */'.format(array_name)]
  207.                 for i in array:
  208.                         idx = i.idx
  209.                         result.append(template_define_udlr(i.name, idx, i.next_u.idx, i.next_d.idx, i.next_l.idx, i.next_r.idx))
  210.                         il = i.enum
  211.                         if il:
  212.                                 enum.append(template_define_enum_member(array_name, il, idx))
  213.                         il = i.label
  214.                         if il:
  215.                                 label.append(template_define_label_value_fragment(il, idx))
  216.                 if enum:
  217.                         result.append('\n/* {0} - enum define */'.format(array_name))
  218.                         result.append(template_define_enum_header(array_name))
  219.                         result.extend(enum)
  220.                         result.append(define_enum_footer)
  221.                 else:
  222.                         result.append('\n/* {0} - enum blank */'.format(array_name))
  223.                 if label:
  224.                         result.append('\n#define DXX_KCONFIG_UI_LABEL_{0}{1}\n'.format(array_name, ''.join(label)))
  225.                 return result
  226.  
  227.         # Given an iterable over a CPP-processed C++ file, find each kc_item
  228.         # array in the file, generate appropriate #define statements, and
  229.         # return the statements as a multiline string.
  230.         #
  231.         # script - path to this script
  232.         #
  233.         # fi - iterable over the CPP-processed C++ file
  234.         #
  235.         # _re_match_defn_const - bound match method for a regular expression
  236.         # to extract a C++ expression from the initializer of a
  237.         # std::integral_constant.
  238.         #
  239.         # _re_match_defn_array - bound match method for a regular expression
  240.         # to match the opening definition of a C++ kc_item array.
  241.         def prepare_text(self,script,fi,
  242.                 _re_match_defn_const=re.compile(r'constexpr\s+std::integral_constant<\w+\s*,\s*((?:\w+\s*\+\s*)*\w+)>\s*(\w+){};').match,
  243.                 _re_match_defn_array=re.compile(r'constexpr\s+kc_item\s+(\w+)\s*\[\]\s*=\s*{').match
  244.                 ):
  245.                 source = fi.name
  246.                 result = ['''/* This is a generated file.  Do not edit.
  247. * This file was generated by {0}
  248. * This file was generated from {1}
  249. */
  250. '''.format(script, source)]
  251.                 lines = []
  252.                 # Simple line reassembly is done automatically, based on the
  253.                 # requirement that braces be balanced to complete an
  254.                 # initializer.  This is required to handle cases where the
  255.                 # initializer split onto multiple lines due to an embedded
  256.                 # preprocessor directive.
  257.                 unbalanced_open_brace = 0
  258.                 array_name = None
  259.                 partial_line = None
  260.                 generate_defines = self.generate_defines
  261.                 for lno, line in enumerate(fi, 1):
  262.                         if line.startswith('#'):
  263.                                 # Ignore line/column position information
  264.                                 continue
  265.                         line = line.strip()
  266.                         if not line:
  267.                                 # Ignore blank lines
  268.                                 continue
  269.                         if line == '};':
  270.                                 # End of array found.  Check for context errors.
  271.                                 # Compute #define statements for this array.  Reset for
  272.                                 # the next array.
  273.                                 if array_name is None:
  274.                                         raise InputException('%s:%u: end of array definition while no array open' % (source, lno))
  275.                                 if unbalanced_open_brace:
  276.                                         raise InputException('%s:%u: end of array definition while reading array initialization' % (source, lno))
  277.                                 result.extend(generate_defines(array_name, source, lines))
  278.                                 lines = []
  279.                                 array_name = None
  280.                                 continue
  281.                         if array_name is None:
  282.                                 # These expressions should never match outside an array
  283.                                 # definition, so apply them only when an array is open.
  284.                                 m = _re_match_defn_const(line)
  285.                                 if m is not None:
  286.                                         # Record a C++ std::integral_constant for later use
  287.                                         # evaluating table cells.
  288.                                         g = m.group
  289.                                         self.constants[g(2)] = self.resolve_expr(source, lno, 'constant', g(1))
  290.                                         continue
  291.                                 m = _re_match_defn_array(line)
  292.                                 if m is not None:
  293.                                         # Array definition found.
  294.                                         array_name = m.group(1)
  295.                                         continue
  296.                         count = line.count
  297.                         unbalanced_open_brace += count('{') - count('}')
  298.                         if unbalanced_open_brace < 0:
  299.                                 raise InputException('%s:%u: brace count becomes negative' % (source, lno))
  300.                         if partial_line is not None:
  301.                                 # Insert a fake whitespace to avoid combining a token at
  302.                                 # the end of one line with a token at the beginning of
  303.                                 # the next.
  304.                                 line = partial_line + ' ' + line
  305.                         if unbalanced_open_brace:
  306.                                 # If braces are unbalanced, assume that this line is
  307.                                 # incomplete, save it for later, and proceed to the next
  308.                                 # line.
  309.                                 partial_line = line
  310.                                 continue
  311.                         partial_line = None
  312.                         lines.append((lno, line))
  313.                 # Check for context error.
  314.                 if array_name is not None:
  315.                         raise InputException('%s: end of file while array definition open' % source)
  316.                 # Ensure end-of-line at end-of-file.
  317.                 result.append('')
  318.                 return '\n'.join(result)
  319.  
  320.         @staticmethod
  321.         def write_generated_text(target,generated_text):
  322.                 if not target:
  323.                         # As a special case, allow this script to be used as a
  324.                         # filter.
  325.                         sys.stdout.write(generated_text)
  326.                         return
  327.                 from tempfile import mkstemp
  328.                 os_path = os.path
  329.                 fd, path = mkstemp(suffix='', prefix='%s.' % os_path.basename(target), dir=os_path.dirname(target), text=True)
  330.                 os.write(fd, generated_text.encode())
  331.                 os.close(fd)
  332.                 os.rename(path, target)
  333.  
  334.         def main(self,script,source,target):
  335.                         # Read the entire file and prepare the output text before
  336.                         # opening the output file.
  337.                 try:
  338.                         with (open(source, 'r') if source else sys.stdin) as fi:
  339.                                 generated_text = self.prepare_text(script,fi)
  340.                 except InputException as e:
  341.                         # Normally, input exceptions are presented as a simple
  342.                         # error message.  If this environment variable is set,
  343.                         # show the full traceback.
  344.                         if os.getenv('DXX_KCONFIG_UDLR_TRACEBACK') is not None:
  345.                                 raise
  346.                         sys.stderr.write('error: %s\n' % e.message)
  347.                         sys.exit(1)
  348.                 self.write_generated_text(target,generated_text)
  349.  
  350. if __name__ == '__main__':
  351.         a = sys.argv
  352.         # As a convenience feature, if no input filename is given, read
  353.         # stdin.  If no output filename is given, write to stdout.
  354.         l = len(a)
  355.         if l < 2:
  356.                 a.append(None)
  357.         if l < 3:
  358.                 a.append(None)
  359.         Main().main(*a[0:3])
  360.