dxx-rebirth/similar/main/generate-kconfig-udlr.py
Kp 569802e00f Convert kconfig array lookups to require correct enumerated type
This ensures that an array is indexed by the appropriate type.  Indexing
a mouse array with a joystick index is undefined behavior, and with this
change, such undefined behavior is now a compilation error.
2020-07-05 23:34:33 +00:00

360 lines
13 KiB
Python

#!/usr/bin/python
import ast, os, re, sys
# Storage for the relevant fields from an initializer for a single
# element of a kc_item array. Fields not required for sorting or
# generating the output header are not captured or saved.
class ArrayInitializationLine:
def __init__(self,xinput,y,name,enum,label,idx):
# xinput, y are fields in the kc_item structure
self.xinput = xinput
self.y = y
# name is the token that will be preprocessor-defined to the
# appropriate udlr values
self.name = name
self.enum = enum
self.label = label
# index in the kc_item array at which this initializer was found
self.idx = idx
# For horizontal sorting, group elements by their vertical
# coordinate, then break ties using the horizontal coordinate. This
# causes elements from a single row to be contiguous in the sorted
# output, and places visually adjacent rows adjacent in the sorted
# output.
def key_horizontal(self):
return (self.y, self.xinput)
# For vertical sorting, group elements by their horizontal
# coordinate, then break ties using the vertical coordinate.
def key_vertical(self):
return (self.xinput, self.y)
# InputException is raised when the post-processed C++ table cannot be
# parsed using the regular expressions in this script. The table may or
# may not be valid C++ when this script rejects it.
class InputException(Exception):
pass
# NodeVisitor handles walking a Python Abstract Syntax Tree (AST) to
# emulate a few supported operations, and raise an error on anything
# unsupported. This is used to implement primitive support for
# evaluating arithmetic expressions in the table. This support covers
# only what is likely to be used and assumes Python syntax (which should
# usually match C++ syntax closely enough, for the limited forms
# supported).
class NodeVisitor(ast.NodeVisitor):
def __init__(self,source,lno,name,expr,constants):
self.source = source
self.lno = lno
self.name = name
self.expr = expr
self.constants = constants
def generic_visit(self,node):
raise InputException('%s:%u: %r expression %r uses unsupported node type %s' % (self.source, self.lno, self.name, self.expr, node.__class__.__name__))
def visit_BinOp(self,node):
left = self.visit(node.left)
right = self.visit(node.right)
op = node.op
if isinstance(op, ast.Add):
return left + right
elif isinstance(op, ast.Sub):
return left - right
elif isinstance(op, ast.Mult):
return left * right
elif isinstance(op, ast.Div):
return left / right
else:
raise InputException('%s:%u: %r expression %r uses unsupported BinOp node type %s' % (self.source, self.lno, self.name, self.expr, op.__class__.__name__))
# Resolve expressions by expanding them.
def visit_Expression(self,node):
return self.visit(node.body)
# Resolve names by searching the dictionary `constants`, which is
# initialized from C++ constexpr declarations found while scanning
# the file.
def visit_Name(self,node):
try:
return self.constants[node.id]
except KeyError as e:
raise InputException('%s:%u: %r expression %r uses undefined name %s' % (self.source, self.lno, self.name, self.expr, node.id))
# Resolve numbers by returning the value as-is.
@staticmethod
def visit_Num(node):
return node.n
class Main:
def __init__(self):
# Initially, no constants are known. Elements will be added by
# prepare_text.
self.constants = {}
# Resolve an expression by parsing it into an AST, then visiting
# the nodes and emulating the permitted operations. Any
# disallowed operations will cause an exception, which will
# propagate through resolve_expr into the caller.
def resolve_expr(self,source,lno,name,expr):
expr = expr.strip()
return NodeVisitor(source, lno, name, expr, self.constants).visit(ast.parse(expr, mode='eval'))
# Given a list of C++ initializer lines, extract from those lines
# the position of each initialized cell, compute the u/d/l/r
# relations among the cells, and return a multiline string
# containing CPP #define statements appropriate to the cells. Each
# C++ initializer must be entirely contained within a single element
# in `lines`.
#
# array_name - C++ identifier for the array; used in a C
# comment and, if labels are used, as part of the name of the macro
# that expands to the label
#
# source - name of the file from which the lines were read
#
# lines - iterable of initializer lines
#
# _re_finditer_init_element - bound method for a regular expression to
# extract the required fields
def generate_defines(self,array_name,source,lines,_re_finditer_init_element=re.compile(r'{'
r'(?:[^,]+),' # x
r'(?P<y>[^,]+),'
r'(?P<xinput>[^,]+),'
r'(?:[^,]+),' # w2
r'\s*(?:DXX_KCONFIG_UI_ENUM\s*\((?P<enum>\w+)\)\s*)?(?:DXX_KCONFIG_UI_LABEL\s*\((?P<label>(?:"[^"]*")|\w*)\)\s*)?(?P<udlr>\w+)\(\),'
r'\s*(?:[^,]+,' # type
r'[^,]+,' # state_bit
r'[^,]+)' # state_ptr
r'},').finditer):
a = ArrayInitializationLine
array = []
append = array.append
resolve_expr = self.resolve_expr
idx = 0
# Iterate over the initializer lines and populate `array` with
# the extracted data.
for lno, line in lines:
ml = _re_finditer_init_element(line)
old_idx = idx
for m in ml:
m = m.group
append(a(
resolve_expr(source, lno, 'xinput', m('xinput')),
resolve_expr(source, lno, 'y', m('y')),
m('udlr'), m('enum'), m('label'), idx))
idx = idx + 1
# If the loop executes zero times, then the regular
# expression failed to match, `old_idx is idx` is True, and
# an exception should be raised, because well-formed input
# will always match at least once.
#
# If the loop executes at least once, then a result was
# found, not `old_idx is idx`, and no exception is necessary.
if old_idx is idx:
raise InputException('%s:%u: failed to match regex for line %r\n' % (source, lno, line))
if not array:
# An empty array is not useful, but may exist when the
# developer is adding a new array and has not yet defined
# any elements.
return '\n/* %s - udlr blank */' % array_name
# Generate a temporary list with the elements sorted for u/d
# navigation. Walk the temporary and assign appropriate u/d
# references.
s = sorted(array, key=a.key_vertical)
p = s[-1]
for i in s:
i.next_u = p
p.next_d = i
p = i
# As above, but sorted for l/r navigation.
s = sorted(array, key=a.key_horizontal)
p = s[-1]
for i in s:
i.next_l = p
p.next_r = i
p = i
# This must be a `#define` since it expands to a comma-separated
# list of values for a structure initializer.
template_define_udlr = '#define {0}()\t/* [{1:2d}] */\t{2:2d},{3:3d},{4:3d},{5:3d}'.format
# Use an `enum class` here so that the values can only be used
# in arrays specifically marked for this index type. This
# prevents mixing types, such as indexing the mouse array with
# joystick index values.
#
# Generate `#define` statements to allow the consuming code to
# use `#ifdef` to detect which members exist.
template_define_enum_header = 'enum class dxx_kconfig_ui_{0} : unsigned {{'.format
template_define_enum_member = '''\
#define dxx_kconfig_ui_{0}_{1} dxx_kconfig_ui_{0}::{1}
{1} = {2},'''.format
define_enum_footer = '};'
template_define_label_value_fragment = '\t\\\n\t/* [{1:2d}] */\t{0} "\\0"'.format
enum = []
label = []
# Generate the `#define` lines using the relations computed by
# the preceding loops. Both ordering loops must execute
# completely before the next_* data required by the udlr
# `#define` lines is available. The udlr logic cannot be folded
# into a prior loop.
#
# The enum member and label logic could be handled in a prior
# loop, but are more logical here. Moving them to an earlier
# loop offers no gain.
result = ['/* {0} - udlr define */'.format(array_name)]
for i in array:
idx = i.idx
result.append(template_define_udlr(i.name, idx, i.next_u.idx, i.next_d.idx, i.next_l.idx, i.next_r.idx))
il = i.enum
if il:
enum.append(template_define_enum_member(array_name, il, idx))
il = i.label
if il:
label.append(template_define_label_value_fragment(il, idx))
if enum:
result.append('\n/* {0} - enum define */'.format(array_name))
result.append(template_define_enum_header(array_name))
result.extend(enum)
result.append(define_enum_footer)
else:
result.append('\n/* {0} - enum blank */'.format(array_name))
if label:
result.append('\n#define DXX_KCONFIG_UI_LABEL_{0}{1}\n'.format(array_name, ''.join(label)))
return result
# Given an iterable over a CPP-processed C++ file, find each kc_item
# array in the file, generate appropriate #define statements, and
# return the statements as a multiline string.
#
# script - path to this script
#
# fi - iterable over the CPP-processed C++ file
#
# _re_match_defn_const - bound match method for a regular expression
# to extract a C++ expression from the initializer of a
# std::integral_constant.
#
# _re_match_defn_array - bound match method for a regular expression
# to match the opening definition of a C++ kc_item array.
def prepare_text(self,script,fi,
_re_match_defn_const=re.compile(r'constexpr\s+std::integral_constant<\w+\s*,\s*((?:\w+\s*\+\s*)*\w+)>\s*(\w+){};').match,
_re_match_defn_array=re.compile(r'constexpr\s+kc_item\s+(\w+)\s*\[\]\s*=\s*{').match
):
source = fi.name
result = ['''/* This is a generated file. Do not edit.
* This file was generated by {0}
* This file was generated from {1}
*/
'''.format(script, source)]
lines = []
# Simple line reassembly is done automatically, based on the
# requirement that braces be balanced to complete an
# initializer. This is required to handle cases where the
# initializer split onto multiple lines due to an embedded
# preprocessor directive.
unbalanced_open_brace = 0
array_name = None
partial_line = None
generate_defines = self.generate_defines
for lno, line in enumerate(fi, 1):
if line.startswith('#'):
# Ignore line/column position information
continue
line = line.strip()
if not line:
# Ignore blank lines
continue
if line == '};':
# End of array found. Check for context errors.
# Compute #define statements for this array. Reset for
# the next array.
if array_name is None:
raise InputException('%s:%u: end of array definition while no array open' % (source, lno))
if unbalanced_open_brace:
raise InputException('%s:%u: end of array definition while reading array initialization' % (source, lno))
result.extend(generate_defines(array_name, source, lines))
lines = []
array_name = None
continue
if array_name is None:
# These expressions should never match outside an array
# definition, so apply them only when an array is open.
m = _re_match_defn_const(line)
if m is not None:
# Record a C++ std::integral_constant for later use
# evaluating table cells.
g = m.group
self.constants[g(2)] = self.resolve_expr(source, lno, 'constant', g(1))
continue
m = _re_match_defn_array(line)
if m is not None:
# Array definition found.
array_name = m.group(1)
continue
count = line.count
unbalanced_open_brace += count('{') - count('}')
if unbalanced_open_brace < 0:
raise InputException('%s:%u: brace count becomes negative' % (source, lno))
if partial_line is not None:
# Insert a fake whitespace to avoid combining a token at
# the end of one line with a token at the beginning of
# the next.
line = partial_line + ' ' + line
if unbalanced_open_brace:
# If braces are unbalanced, assume that this line is
# incomplete, save it for later, and proceed to the next
# line.
partial_line = line
continue
partial_line = None
lines.append((lno, line))
# Check for context error.
if array_name is not None:
raise InputException('%s: end of file while array definition open' % source)
# Ensure end-of-line at end-of-file.
result.append('')
return '\n'.join(result)
@staticmethod
def write_generated_text(target,generated_text):
if not target:
# As a special case, allow this script to be used as a
# filter.
sys.stdout.write(generated_text)
return
from tempfile import mkstemp
os_path = os.path
fd, path = mkstemp(suffix='', prefix='%s.' % os_path.basename(target), dir=os_path.dirname(target), text=True)
os.write(fd, generated_text.encode())
os.close(fd)
os.rename(path, target)
def main(self,script,source,target):
# Read the entire file and prepare the output text before
# opening the output file.
try:
with (open(source, 'r') if source else sys.stdin) as fi:
generated_text = self.prepare_text(script,fi)
except InputException as e:
# Normally, input exceptions are presented as a simple
# error message. If this environment variable is set,
# show the full traceback.
if os.getenv('DXX_KCONFIG_UDLR_TRACEBACK') is not None:
raise
sys.stderr.write('error: %s\n' % e.message)
sys.exit(1)
self.write_generated_text(target,generated_text)
if __name__ == '__main__':
a = sys.argv
# As a convenience feature, if no input filename is given, read
# stdin. If no output filename is given, write to stdout.
l = len(a)
if l < 2:
a.append(None)
if l < 3:
a.append(None)
Main().main(*a[0:3])