Subversion Repositories Games.Descent

Rev

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])