Details | Last modification | View Log | RSS feed
| Rev | Author | Line No. | Line |
|---|---|---|---|
| 1 | pmbaty | 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]) |