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