diff options
author | 2023-10-10 11:40:56 +0000 | |
---|---|---|
committer | 2023-10-10 11:40:56 +0000 | |
commit | e02cda008591317b1625707ff8e115a4841aa889 (patch) | |
tree | aee302e3cf8b59ec2d32ec481be3d1afddfc8968 /scripts/codeconverter | |
parent | cc668e6b7e0ffd8c9d130513d12053cf5eda1d3b (diff) |
Introduce Virtio-loopback epsilon release:
Epsilon release introduces a new compatibility layer which make virtio-loopback
design to work with QEMU and rust-vmm vhost-user backend without require any
changes.
Signed-off-by: Timos Ampelikiotis <t.ampelikiotis@virtualopensystems.com>
Change-Id: I52e57563e08a7d0bdc002f8e928ee61ba0c53dd9
Diffstat (limited to 'scripts/codeconverter')
-rw-r--r-- | scripts/codeconverter/codeconverter/__init__.py | 0 | ||||
-rw-r--r-- | scripts/codeconverter/codeconverter/patching.py | 466 | ||||
-rw-r--r-- | scripts/codeconverter/codeconverter/qom_macros.py | 861 | ||||
-rw-r--r-- | scripts/codeconverter/codeconverter/qom_type_info.py | 969 | ||||
-rw-r--r-- | scripts/codeconverter/codeconverter/regexps.py | 118 | ||||
-rw-r--r-- | scripts/codeconverter/codeconverter/test_patching.py | 104 | ||||
-rw-r--r-- | scripts/codeconverter/codeconverter/test_regexps.py | 282 | ||||
-rw-r--r-- | scripts/codeconverter/codeconverter/utils.py | 72 | ||||
-rwxr-xr-x | scripts/codeconverter/converter.py | 123 |
9 files changed, 2995 insertions, 0 deletions
diff --git a/scripts/codeconverter/codeconverter/__init__.py b/scripts/codeconverter/codeconverter/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/scripts/codeconverter/codeconverter/__init__.py diff --git a/scripts/codeconverter/codeconverter/patching.py b/scripts/codeconverter/codeconverter/patching.py new file mode 100644 index 000000000..9e92505d3 --- /dev/null +++ b/scripts/codeconverter/codeconverter/patching.py @@ -0,0 +1,466 @@ +# Copyright (C) 2020 Red Hat Inc. +# +# Authors: +# Eduardo Habkost <ehabkost@redhat.com> +# +# This work is licensed under the terms of the GNU GPL, version 2. See +# the COPYING file in the top-level directory. +from typing import IO, Match, NamedTuple, Optional, Literal, Iterable, Type, Dict, List, Any, TypeVar, NewType, Tuple, Union +from pathlib import Path +from itertools import chain +from tempfile import NamedTemporaryFile +import os +import re +import subprocess +from io import StringIO + +import logging +logger = logging.getLogger(__name__) +DBG = logger.debug +INFO = logger.info +WARN = logger.warning +ERROR = logger.error + +from .utils import * + +T = TypeVar('T') + +class Patch(NamedTuple): + # start inside file.original_content + start: int + # end position inside file.original_content + end: int + # replacement string for file.original_content[start:end] + replacement: str + +IdentifierType = Literal['type', 'symbol', 'include', 'constant'] +class RequiredIdentifier(NamedTuple): + type: IdentifierType + name: str + +class FileMatch: + """Base class for regex matches + + Subclasses just need to set the `regexp` class attribute + """ + regexp: Optional[str] = None + + def __init__(self, f: 'FileInfo', m: Match) -> None: + self.file: 'FileInfo' = f + self.match: Match[str] = m + + @property + def name(self) -> str: + if 'name' not in self.match.groupdict(): + return '[no name]' + return self.group('name') + + @classmethod + def compiled_re(klass): + return re.compile(klass.regexp, re.MULTILINE) + + def start(self) -> int: + return self.match.start() + + def end(self) -> int: + return self.match.end() + + def line_col(self) -> LineAndColumn: + return self.file.line_col(self.start()) + + def group(self, group: Union[int, str]) -> str: + return self.match.group(group) + + def getgroup(self, group: str) -> Optional[str]: + if group not in self.match.groupdict(): + return None + return self.match.group(group) + + def log(self, level, fmt, *args) -> None: + pos = self.line_col() + logger.log(level, '%s:%d:%d: '+fmt, self.file.filename, pos.line, pos.col, *args) + + def debug(self, fmt, *args) -> None: + self.log(logging.DEBUG, fmt, *args) + + def info(self, fmt, *args) -> None: + self.log(logging.INFO, fmt, *args) + + def warn(self, fmt, *args) -> None: + self.log(logging.WARNING, fmt, *args) + + def error(self, fmt, *args) -> None: + self.log(logging.ERROR, fmt, *args) + + def sub(self, original: str, replacement: str) -> str: + """Replace content + + XXX: this won't use the match position, but will just + replace all strings that look like the original match. + This should be enough for all the patterns used in this + script. + """ + return original.replace(self.group(0), replacement) + + def sanity_check(self) -> None: + """Sanity check match, and print warnings if necessary""" + pass + + def replacement(self) -> Optional[str]: + """Return replacement text for pattern, to use new code conventions""" + return None + + def make_patch(self, replacement: str) -> 'Patch': + """Make patch replacing the content of this match""" + return Patch(self.start(), self.end(), replacement) + + def make_subpatch(self, start: int, end: int, replacement: str) -> 'Patch': + return Patch(self.start() + start, self.start() + end, replacement) + + def make_removal_patch(self) -> 'Patch': + """Make patch removing contents of match completely""" + return self.make_patch('') + + def append(self, s: str) -> 'Patch': + """Make patch appending string after this match""" + return Patch(self.end(), self.end(), s) + + def prepend(self, s: str) -> 'Patch': + """Make patch prepending string before this match""" + return Patch(self.start(), self.start(), s) + + def gen_patches(self) -> Iterable['Patch']: + """Patch source code contents to use new code patterns""" + replacement = self.replacement() + if replacement is not None: + yield self.make_patch(replacement) + + @classmethod + def has_replacement_rule(klass) -> bool: + return (klass.gen_patches is not FileMatch.gen_patches + or klass.replacement is not FileMatch.replacement) + + def contains(self, other: 'FileMatch') -> bool: + return other.start() >= self.start() and other.end() <= self.end() + + def __repr__(self) -> str: + start = self.file.line_col(self.start()) + end = self.file.line_col(self.end() - 1) + return '<%s %s at %d:%d-%d:%d: %r>' % (self.__class__.__name__, + self.name, + start.line, start.col, + end.line, end.col, self.group(0)[:100]) + + def required_identifiers(self) -> Iterable[RequiredIdentifier]: + """Can be implemented by subclasses to keep track of identifier references + + This method will be used by the code that moves declarations around the file, + to make sure we find the right spot for them. + """ + raise NotImplementedError() + + def provided_identifiers(self) -> Iterable[RequiredIdentifier]: + """Can be implemented by subclasses to keep track of identifier references + + This method will be used by the code that moves declarations around the file, + to make sure we find the right spot for them. + """ + raise NotImplementedError() + + @classmethod + def finditer(klass, content: str, pos=0, endpos=-1) -> Iterable[Match]: + """Helper for re.finditer()""" + if endpos >= 0: + content = content[:endpos] + return klass.compiled_re().finditer(content, pos) + + @classmethod + def domatch(klass, content: str, pos=0, endpos=-1) -> Optional[Match]: + """Helper for re.match()""" + if endpos >= 0: + content = content[:endpos] + return klass.compiled_re().match(content, pos) + + def group_finditer(self, klass: Type['FileMatch'], group: Union[str, int]) -> Iterable['FileMatch']: + assert self.file.original_content + return (klass(self.file, m) + for m in klass.finditer(self.file.original_content, + self.match.start(group), + self.match.end(group))) + + def try_group_match(self, klass: Type['FileMatch'], group: Union[str, int]) -> Optional['FileMatch']: + assert self.file.original_content + m = klass.domatch(self.file.original_content, + self.match.start(group), + self.match.end(group)) + if not m: + return None + else: + return klass(self.file, m) + + def group_match(self, group: Union[str, int]) -> 'FileMatch': + m = self.try_group_match(FullMatch, group) + assert m + return m + + @property + def allfiles(self) -> 'FileList': + return self.file.allfiles + +class FullMatch(FileMatch): + """Regexp that will match all contents of string + Useful when used with group_match() + """ + regexp = r'(?s).*' # (?s) is re.DOTALL + +def all_subclasses(c: Type[FileMatch]) -> Iterable[Type[FileMatch]]: + for sc in c.__subclasses__(): + yield sc + yield from all_subclasses(sc) + +def match_class_dict() -> Dict[str, Type[FileMatch]]: + d = dict((t.__name__, t) for t in all_subclasses(FileMatch)) + return d + +def names(matches: Iterable[FileMatch]) -> Iterable[str]: + return [m.name for m in matches] + +class PatchingError(Exception): + pass + +class OverLappingPatchesError(PatchingError): + pass + +def apply_patches(s: str, patches: Iterable[Patch]) -> str: + """Apply a sequence of patches to string + + >>> apply_patches('abcdefg', [Patch(2,2,'xxx'), Patch(0, 1, 'yy')]) + 'yybxxxcdefg' + """ + r = StringIO() + last = 0 + def patch_sort_key(item: Tuple[int, Patch]) -> Tuple[int, int, int]: + """Patches are sorted by byte position, + patches at the same byte position are applied in the order + they were generated. + """ + i,p = item + return (p.start, p.end, i) + + for i,p in sorted(enumerate(patches), key=patch_sort_key): + DBG("Applying patch at position %d (%s) - %d (%s): %r", + p.start, line_col(s, p.start), + p.end, line_col(s, p.end), + p.replacement) + if last > p.start: + raise OverLappingPatchesError("Overlapping patch at position %d (%s), last patch at %d (%s)" % \ + (p.start, line_col(s, p.start), last, line_col(s, last))) + r.write(s[last:p.start]) + r.write(p.replacement) + last = p.end + r.write(s[last:]) + return r.getvalue() + +class RegexpScanner: + def __init__(self) -> None: + self.match_index: Dict[Type[Any], List[FileMatch]] = {} + self.match_name_index: Dict[Tuple[Type[Any], str, str], Optional[FileMatch]] = {} + + def _matches_of_type(self, klass: Type[Any]) -> Iterable[FileMatch]: + raise NotImplementedError() + + def matches_of_type(self, t: Type[T]) -> List[T]: + if t not in self.match_index: + self.match_index[t] = list(self._matches_of_type(t)) + return self.match_index[t] # type: ignore + + def find_matches(self, t: Type[T], name: str, group: str='name') -> List[T]: + indexkey = (t, name, group) + if indexkey in self.match_name_index: + return self.match_name_index[indexkey] # type: ignore + r: List[T] = [] + for m in self.matches_of_type(t): + assert isinstance(m, FileMatch) + if m.getgroup(group) == name: + r.append(m) # type: ignore + self.match_name_index[indexkey] = r # type: ignore + return r + + def find_match(self, t: Type[T], name: str, group: str='name') -> Optional[T]: + l = self.find_matches(t, name, group) + if not l: + return None + if len(l) > 1: + logger.warn("multiple matches found for %r (%s=%r)", t, group, name) + return None + return l[0] + + def reset_index(self) -> None: + self.match_index.clear() + self.match_name_index.clear() + +class FileInfo(RegexpScanner): + filename: Path + original_content: Optional[str] = None + + def __init__(self, files: 'FileList', filename: os.PathLike, force:bool=False) -> None: + super().__init__() + self.allfiles = files + self.filename = Path(filename) + self.patches: List[Patch] = [] + self.force = force + + def __repr__(self) -> str: + return f'<FileInfo {repr(self.filename)}>' + + def filename_matches(self, name: str) -> bool: + nameparts = Path(name).parts + return self.filename.parts[-len(nameparts):] == nameparts + + def line_col(self, start: int) -> LineAndColumn: + """Return line and column for a match object inside original_content""" + return line_col(self.original_content, start) + + def _matches_of_type(self, klass: Type[Any]) -> List[FileMatch]: + """Build FileMatch objects for each match of regexp""" + if not hasattr(klass, 'regexp') or klass.regexp is None: + return [] + assert hasattr(klass, 'regexp') + DBG("%s: scanning for %s", self.filename, klass.__name__) + DBG("regexp: %s", klass.regexp) + matches = [klass(self, m) for m in klass.finditer(self.original_content)] + DBG('%s: %d matches found for %s: %s', self.filename, len(matches), + klass.__name__,' '.join(names(matches))) + return matches + + def find_match(self, t: Type[T], name: str, group: str='name') -> Optional[T]: + for m in self.matches_of_type(t): + assert isinstance(m, FileMatch) + if m.getgroup(group) == name: + return m # type: ignore + return None + + def reset_content(self, s:str): + self.original_content = s + self.patches.clear() + self.reset_index() + self.allfiles.reset_index() + + def load(self) -> None: + if self.original_content is not None: + return + with open(self.filename, 'rt') as f: + self.reset_content(f.read()) + + @property + def all_matches(self) -> Iterable[FileMatch]: + lists = list(self.match_index.values()) + return (m for l in lists + for m in l) + + def gen_patches(self, matches: List[FileMatch]) -> None: + for m in matches: + DBG("Generating patches for %r", m) + for i,p in enumerate(m.gen_patches()): + DBG("patch %d generated by %r:", i, m) + DBG("replace contents at %s-%s with %r", + self.line_col(p.start), self.line_col(p.end), p.replacement) + self.patches.append(p) + + def scan_for_matches(self, class_names: Optional[List[str]]=None) -> Iterable[FileMatch]: + DBG("class names: %r", class_names) + class_dict = match_class_dict() + if class_names is None: + DBG("default class names") + class_names = list(name for name,klass in class_dict.items() + if klass.has_replacement_rule()) + DBG("class_names: %r", class_names) + for cn in class_names: + matches = self.matches_of_type(class_dict[cn]) + DBG('%d matches found for %s: %s', + len(matches), cn, ' '.join(names(matches))) + yield from matches + + def apply_patches(self) -> None: + """Replace self.original_content after applying patches from self.patches""" + self.reset_content(self.get_patched_content()) + + def get_patched_content(self) -> str: + assert self.original_content is not None + return apply_patches(self.original_content, self.patches) + + def write_to_file(self, f: IO[str]) -> None: + f.write(self.get_patched_content()) + + def write_to_filename(self, filename: os.PathLike) -> None: + with open(filename, 'wt') as of: + self.write_to_file(of) + + def patch_inplace(self) -> None: + newfile = self.filename.with_suffix('.changed') + self.write_to_filename(newfile) + os.rename(newfile, self.filename) + + def show_diff(self) -> None: + with NamedTemporaryFile('wt') as f: + self.write_to_file(f) + f.flush() + subprocess.call(['diff', '-u', self.filename, f.name]) + + def ref(self): + return TypeInfoReference + +class FileList(RegexpScanner): + def __init__(self): + super().__init__() + self.files: List[FileInfo] = [] + + def extend(self, *args, **kwargs): + self.files.extend(*args, **kwargs) + + def __iter__(self): + return iter(self.files) + + def _matches_of_type(self, klass: Type[Any]) -> Iterable[FileMatch]: + return chain(*(f._matches_of_type(klass) for f in self.files)) + + def find_file(self, name: str) -> Optional[FileInfo]: + """Get file with path ending with @name""" + for f in self.files: + if f.filename_matches(name): + return f + else: + return None + + def one_pass(self, class_names: List[str]) -> int: + total_patches = 0 + for f in self.files: + INFO("Scanning file %s", f.filename) + matches = list(f.scan_for_matches(class_names)) + INFO("Generating patches for file %s", f.filename) + f.gen_patches(matches) + total_patches += len(f.patches) + if total_patches: + for f in self.files: + try: + f.apply_patches() + except PatchingError: + logger.exception("%s: failed to patch file", f.filename) + return total_patches + + def patch_content(self, max_passes, class_names: List[str]) -> None: + """Multi-pass content patching loop + + We run multiple passes because there are rules that will + delete init functions once they become empty. + """ + passes = 0 + total_patches = 0 + DBG("max_passes: %r", max_passes) + while not max_passes or max_passes <= 0 or passes < max_passes: + passes += 1 + INFO("Running pass: %d", passes) + count = self.one_pass(class_names) + DBG("patch content: pass %d: %d patches generated", passes, count) + total_patches += count + DBG("%d patches applied total in %d passes", total_patches, passes) diff --git a/scripts/codeconverter/codeconverter/qom_macros.py b/scripts/codeconverter/codeconverter/qom_macros.py new file mode 100644 index 000000000..2d2f2055a --- /dev/null +++ b/scripts/codeconverter/codeconverter/qom_macros.py @@ -0,0 +1,861 @@ +# Copyright (C) 2020 Red Hat Inc. +# +# Authors: +# Eduardo Habkost <ehabkost@redhat.com> +# +# This work is licensed under the terms of the GNU GPL, version 2. See +# the COPYING file in the top-level directory. +import re +from itertools import chain +from typing import * + +from .regexps import * +from .patching import * +from .utils import * + +import logging +logger = logging.getLogger(__name__) +DBG = logger.debug +INFO = logger.info +WARN = logger.warning + +# simple expressions: + +RE_CONSTANT = OR(RE_STRING, RE_NUMBER) + +class DefineDirective(FileMatch): + """Match any #define directive""" + regexp = S(r'^[ \t]*#[ \t]*define', CPP_SPACE, NAMED('name', RE_IDENTIFIER), r'\b') + +class ExpressionDefine(FileMatch): + """Simple #define preprocessor directive for an expression""" + regexp = S(r'^[ \t]*#[ \t]*define', CPP_SPACE, NAMED('name', RE_IDENTIFIER), + CPP_SPACE, NAMED('value', RE_EXPRESSION), r'[ \t]*\n') + + def provided_identifiers(self) -> Iterable[RequiredIdentifier]: + yield RequiredIdentifier('constant', self.group('name')) + +class ConstantDefine(ExpressionDefine): + """Simple #define preprocessor directive for a number or string constant""" + regexp = S(r'^[ \t]*#[ \t]*define', CPP_SPACE, NAMED('name', RE_IDENTIFIER), + CPP_SPACE, NAMED('value', RE_CONSTANT), r'[ \t]*\n') + + +class TypeIdentifiers(NamedTuple): + """Type names found in type declarations""" + # TYPE_MYDEVICE + typename: Optional[str] + # MYDEVICE + uppercase: Optional[str] = None + # MyDevice + instancetype: Optional[str] = None + # MyDeviceClass + classtype: Optional[str] = None + # my_device + lowercase: Optional[str] = None + + def allfields(self): + return tuple(getattr(self, f) for f in self._fields) + + def merge(self, other: 'TypeIdentifiers') -> Optional['TypeIdentifiers']: + """Check if identifiers match, return new identifier with complete list""" + if any(not opt_compare(a, b) for a,b in zip(self, other)): + return None + return TypeIdentifiers(*(merge(a, b) for a,b in zip(self, other))) + + def __str__(self) -> str: + values = ((f, getattr(self, f)) for f in self._fields) + s = ', '.join('%s=%s' % (f,v) for f,v in values if v is not None) + return f'{s}' + + def check_consistency(self) -> List[str]: + """Check if identifiers are consistent with each other, + return list of problems (or empty list if everything seems consistent) + """ + r = [] + if self.typename is None: + r.append("typename (TYPE_MYDEVICE) is unavailable") + + if self.uppercase is None: + r.append("uppercase name is unavailable") + + if (self.instancetype is not None + and self.classtype is not None + and self.classtype != f'{self.instancetype}Class'): + r.append("class typedef %s doesn't match instance typedef %s" % + (self.classtype, self.instancetype)) + + if (self.uppercase is not None + and self.typename is not None + and f'TYPE_{self.uppercase}' != self.typename): + r.append("uppercase name (%s) doesn't match type name (%s)" % + (self.uppercase, self.typename)) + + return r + +class TypedefMatch(FileMatch): + """typedef declaration""" + def provided_identifiers(self) -> Iterable[RequiredIdentifier]: + yield RequiredIdentifier('type', self.group('name')) + +class SimpleTypedefMatch(TypedefMatch): + """Simple typedef declaration + (no replacement rules)""" + regexp = S(r'^[ \t]*typedef', SP, + NAMED('typedef_type', RE_TYPE), SP, + NAMED('name', RE_IDENTIFIER), r'\s*;[ \t]*\n') + +RE_MACRO_DEFINE = S(r'^[ \t]*#\s*define\s+', NAMED('name', RE_IDENTIFIER), + r'\s*\(\s*', RE_IDENTIFIER, r'\s*\)', CPP_SPACE) + +RE_STRUCT_ATTRIBUTE = r'QEMU_PACKED' + +# This doesn't parse the struct definitions completely, it just assumes +# the closing brackets are going to be in an unindented line: +RE_FULL_STRUCT = S('struct', SP, M(RE_IDENTIFIER, n='?', name='structname'), SP, + NAMED('body', r'{\n', + # acceptable inside the struct body: + # - lines starting with space or tab + # - empty lines + # - preprocessor directives + # - comments + OR(r'[ \t][^\n]*\n', + r'#[^\n]*\n', + r'\n', + S(r'[ \t]*', RE_COMMENT, r'[ \t]*\n'), + repeat='*?'), + r'}', M(RE_STRUCT_ATTRIBUTE, SP, n='*'))) +RE_STRUCT_TYPEDEF = S(r'^[ \t]*typedef', SP, RE_FULL_STRUCT, SP, + NAMED('name', RE_IDENTIFIER), r'\s*;[ \t]*\n') + +class FullStructTypedefMatch(TypedefMatch): + """typedef struct [SomeStruct] { ...} SomeType + Will be replaced by separate struct declaration + typedef + """ + regexp = RE_STRUCT_TYPEDEF + + def make_structname(self) -> str: + """Make struct name for struct+typedef split""" + name = self.group('structname') + if not name: + name = self.name + return name + + def strip_typedef(self) -> Patch: + """generate patch that will strip typedef from the struct declartion + + The caller is responsible for readding the typedef somewhere else. + """ + name = self.make_structname() + body = self.group('body') + return self.make_patch(f'struct {name} {body};\n') + + def make_simple_typedef(self) -> str: + structname = self.make_structname() + name = self.name + return f'typedef struct {structname} {name};\n' + + def move_typedef(self, position) -> Iterator[Patch]: + """Generate patches to move typedef elsewhere""" + yield self.strip_typedef() + yield Patch(position, position, self.make_simple_typedef()) + + def split_typedef(self) -> Iterator[Patch]: + """Split into struct definition + typedef in-place""" + yield self.strip_typedef() + yield self.append(self.make_simple_typedef()) + +class StructTypedefSplit(FullStructTypedefMatch): + """split struct+typedef declaration""" + def gen_patches(self) -> Iterator[Patch]: + if self.group('structname'): + yield from self.split_typedef() + +class DuplicatedTypedefs(SimpleTypedefMatch): + """Delete ALL duplicate typedefs (unsafe)""" + def gen_patches(self) -> Iterable[Patch]: + other_td = [td for td in chain(self.file.matches_of_type(SimpleTypedefMatch), + self.file.matches_of_type(FullStructTypedefMatch)) + if td.name == self.name] + DBG("other_td: %r", other_td) + if any(td.start() < self.start() for td in other_td): + # patch only if handling the first typedef + return + for td in other_td: + if isinstance(td, SimpleTypedefMatch): + DBG("other td: %r", td.match.groupdict()) + if td.group('typedef_type') != self.group('typedef_type'): + yield td.make_removal_patch() + elif isinstance(td, FullStructTypedefMatch): + DBG("other td: %r", td.match.groupdict()) + if self.group('typedef_type') == 'struct '+td.group('structname'): + yield td.strip_typedef() + +class QOMDuplicatedTypedefs(DuplicatedTypedefs): + """Delete duplicate typedefs if used by QOM type""" + def gen_patches(self) -> Iterable[Patch]: + qom_macros = [TypeCheckMacro, DeclareInstanceChecker, DeclareClassCheckers, DeclareObjCheckers] + qom_matches = chain(*(self.file.matches_of_type(t) for t in qom_macros)) + in_use = any(RequiredIdentifier('type', self.name) in m.required_identifiers() + for m in qom_matches) + if in_use: + yield from DuplicatedTypedefs.gen_patches(self) + +class QOMStructTypedefSplit(FullStructTypedefMatch): + """split struct+typedef declaration if used by QOM type""" + def gen_patches(self) -> Iterator[Patch]: + qom_macros = [TypeCheckMacro, DeclareInstanceChecker, DeclareClassCheckers, DeclareObjCheckers] + qom_matches = chain(*(self.file.matches_of_type(t) for t in qom_macros)) + in_use = any(RequiredIdentifier('type', self.name) in m.required_identifiers() + for m in qom_matches) + if in_use: + yield from self.split_typedef() + +def typedefs(file: FileInfo) -> Iterable[TypedefMatch]: + return (cast(TypedefMatch, m) + for m in chain(file.matches_of_type(SimpleTypedefMatch), + file.matches_of_type(FullStructTypedefMatch))) + +def find_typedef(f: FileInfo, name: Optional[str]) -> Optional[TypedefMatch]: + if not name: + return None + for td in typedefs(f): + if td.name == name: + return td + return None + +CHECKER_MACROS = ['OBJECT_CHECK', 'OBJECT_CLASS_CHECK', 'OBJECT_GET_CLASS'] +CheckerMacroName = Literal['OBJECT_CHECK', 'OBJECT_CLASS_CHECK', 'OBJECT_GET_CLASS'] + +RE_CHECK_MACRO = \ + S(RE_MACRO_DEFINE, + OR(*CHECKER_MACROS, name='checker'), + M(r'\s*\(\s*', OR(NAMED('typedefname', RE_IDENTIFIER), RE_TYPE, name='c_type'), r'\s*,', CPP_SPACE, + OPTIONAL_PARS(RE_IDENTIFIER), r',', CPP_SPACE, + NAMED('qom_typename', RE_IDENTIFIER), r'\s*\)\n', + n='?', name='check_args')) + +EXPECTED_CHECKER_SUFFIXES: List[Tuple[CheckerMacroName, str]] = [ + ('OBJECT_GET_CLASS', '_GET_CLASS'), + ('OBJECT_CLASS_CHECK', '_CLASS'), +] + +class TypeCheckMacro(FileMatch): + """OBJECT_CHECK/OBJECT_CLASS_CHECK/OBJECT_GET_CLASS macro definitions + Will be replaced by DECLARE_*_CHECKERS macro + """ + regexp = RE_CHECK_MACRO + + @property + def checker(self) -> CheckerMacroName: + """Name of checker macro being used""" + return self.group('checker') # type: ignore + + @property + def typedefname(self) -> Optional[str]: + return self.group('typedefname') + + def find_typedef(self) -> Optional[TypedefMatch]: + return find_typedef(self.file, self.typedefname) + + def sanity_check(self) -> None: + DBG("groups: %r", self.match.groups()) + if not self.group('check_args'): + self.warn("type check macro not parsed completely: %s", self.name) + return + DBG("type identifiers: %r", self.type_identifiers) + if self.typedefname and self.find_typedef() is None: + self.warn("typedef used by %s not found", self.name) + + def find_matching_macros(self) -> List['TypeCheckMacro']: + """Find other check macros that generate the same macro names + + The returned list will always be sorted. + """ + my_ids = self.type_identifiers + assert my_ids + return [m for m in self.file.matches_of_type(TypeCheckMacro) + if m.type_identifiers is not None + and my_ids.uppercase is not None + and (my_ids.uppercase == m.type_identifiers.uppercase + or my_ids.typename == m.type_identifiers.typename)] + + def merge_ids(self, matches: List['TypeCheckMacro']) -> Optional[TypeIdentifiers]: + """Try to merge info about type identifiers from all matches in a list""" + if not matches: + return None + r = matches[0].type_identifiers + if r is None: + return None + for m in matches[1:]: + assert m.type_identifiers + new = r.merge(m.type_identifiers) + if new is None: + self.warn("macro %s identifiers (%s) don't match macro %s (%s)", + matches[0].name, r, m.name, m.type_identifiers) + return None + r = new + return r + + def required_identifiers(self) -> Iterable[RequiredIdentifier]: + yield RequiredIdentifier('include', '"qom/object.h"') + if self.type_identifiers is None: + return + # to make sure typedefs will be moved above all related macros, + # return dependencies from all of them, not just this match + for m in self.find_matching_macros(): + yield RequiredIdentifier('type', m.group('c_type')) + yield RequiredIdentifier('constant', m.group('qom_typename')) + + @property + def type_identifiers(self) -> Optional[TypeIdentifiers]: + """Extract type identifier information from match""" + typename = self.group('qom_typename') + c_type = self.group('c_type') + if not typename or not c_type: + return None + typedef = self.group('typedefname') + classtype = None + instancetype = None + uppercase = None + expected_suffix = dict(EXPECTED_CHECKER_SUFFIXES).get(self.checker) + + # here the available data depends on the checker macro being called: + # - we need to remove the suffix from the macro name + # - depending on the macro type, we know the class type name, or + # the instance type name + if self.checker in ('OBJECT_GET_CLASS', 'OBJECT_CLASS_CHECK'): + classtype = c_type + elif self.checker == 'OBJECT_CHECK': + instancetype = c_type + uppercase = self.name + else: + assert False + if expected_suffix and self.name.endswith(expected_suffix): + uppercase = self.name[:-len(expected_suffix)] + return TypeIdentifiers(typename=typename, classtype=classtype, + instancetype=instancetype, uppercase=uppercase) + + def gen_patches(self) -> Iterable[Patch]: + # the implementation is a bit tricky because we need to group + # macros dealing with the same type into a single declaration + if self.type_identifiers is None: + self.warn("couldn't extract type information from macro %s", self.name) + return + + if self.name == 'INTERFACE_CLASS': + # INTERFACE_CLASS is special and won't be patched + return + + for checker,suffix in EXPECTED_CHECKER_SUFFIXES: + if self.name.endswith(suffix): + if self.checker != checker: + self.warn("macro %s is using macro %s instead of %s", self.name, self.checker, checker) + return + break + + matches = self.find_matching_macros() + DBG("found %d matching macros: %s", len(matches), ' '.join(m.name for m in matches)) + # we will generate patches only when processing the first macro: + if matches[0].start != self.start: + DBG("skipping %s (will patch when handling %s)", self.name, matches[0].name) + return + + + ids = self.merge_ids(matches) + if ids is None: + DBG("type identifier mismatch, won't patch %s", self.name) + return + + if not ids.uppercase: + self.warn("macro %s doesn't follow the expected name pattern", self.name) + return + if not ids.typename: + self.warn("macro %s: couldn't extract type name", self.name) + return + + #issues = ids.check_consistency() + #if issues: + # for i in issues: + # self.warn("inconsistent identifiers: %s", i) + + names = [n for n in (ids.instancetype, ids.classtype, ids.uppercase, ids.typename) + if n is not None] + if len(set(names)) != len(names): + self.warn("duplicate names used by macro: %r", ids) + return + + assert ids.classtype or ids.instancetype + assert ids.typename + assert ids.uppercase + if ids.classtype and ids.instancetype: + new_decl = (f'DECLARE_OBJ_CHECKERS({ids.instancetype}, {ids.classtype},\n' + f' {ids.uppercase}, {ids.typename})\n') + elif ids.classtype: + new_decl = (f'DECLARE_CLASS_CHECKERS({ids.classtype}, {ids.uppercase},\n' + f' {ids.typename})\n') + elif ids.instancetype: + new_decl = (f'DECLARE_INSTANCE_CHECKER({ids.instancetype}, {ids.uppercase},\n' + f' {ids.typename})\n') + else: + assert False + + # we need to ensure the typedefs are already available + issues = [] + for t in [ids.instancetype, ids.classtype]: + if not t: + continue + if re.fullmatch(RE_STRUCT_TYPE, t): + self.info("type %s is not a typedef", t) + continue + td = find_typedef(self.file, t) + #if not td and self.allfiles.find_file('include/qemu/typedefs.h'): + # + if not td: + # it is OK if the typedef is in typedefs.h + f = self.allfiles.find_file('include/qemu/typedefs.h') + if f and find_typedef(f, t): + self.info("typedef %s found in typedefs.h", t) + continue + + issues.append("couldn't find typedef %s" % (t)) + elif td.start() > self.start(): + issues.append("typedef %s need to be moved earlier in the file" % (td.name)) + + for issue in issues: + self.warn(issue) + + if issues and not self.file.force: + return + + # delete all matching macros and add new declaration: + for m in matches: + yield m.make_patch('') + for issue in issues: + yield self.prepend("/* FIXME: %s */\n" % (issue)) + yield self.append(new_decl) + +class InterfaceCheckMacro(FileMatch): + """Type checking macro using INTERFACE_CHECK + Will be replaced by DECLARE_INTERFACE_CHECKER + """ + regexp = S(RE_MACRO_DEFINE, + 'INTERFACE_CHECK', + r'\s*\(\s*', OR(NAMED('instancetype', RE_IDENTIFIER), RE_TYPE, name='c_type'), + r'\s*,', CPP_SPACE, + OPTIONAL_PARS(RE_IDENTIFIER), r',', CPP_SPACE, + NAMED('qom_typename', RE_IDENTIFIER), r'\s*\)\n') + + def required_identifiers(self) -> Iterable[RequiredIdentifier]: + yield RequiredIdentifier('include', '"qom/object.h"') + yield RequiredIdentifier('type', self.group('instancetype')) + yield RequiredIdentifier('constant', self.group('qom_typename')) + + def gen_patches(self) -> Iterable[Patch]: + if self.file.filename_matches('qom/object.h'): + self.debug("skipping object.h") + return + + typename = self.group('qom_typename') + uppercase = self.name + instancetype = self.group('instancetype') + c = f"DECLARE_INTERFACE_CHECKER({instancetype}, {uppercase},\n"+\ + f" {typename})\n" + yield self.make_patch(c) + + +class TypeDeclaration(FileMatch): + """Parent class to all type declarations""" + @property + def instancetype(self) -> Optional[str]: + return self.getgroup('instancetype') + + @property + def classtype(self) -> Optional[str]: + return self.getgroup('classtype') + + @property + def typename(self) -> Optional[str]: + return self.getgroup('typename') + +class TypeCheckerDeclaration(TypeDeclaration): + """Parent class to all type checker declarations""" + @property + def typename(self) -> str: + return self.group('typename') + + @property + def uppercase(self) -> str: + return self.group('uppercase') + +class DeclareInstanceChecker(TypeCheckerDeclaration): + """DECLARE_INSTANCE_CHECKER use""" + #TODO: replace lonely DECLARE_INSTANCE_CHECKER with DECLARE_OBJ_CHECKERS + # if all types are found. + # This will require looking up the correct class type in the TypeInfo + # structs in another file + regexp = S(r'^[ \t]*DECLARE_INSTANCE_CHECKER\s*\(\s*', + NAMED('instancetype', RE_TYPE), r'\s*,\s*', + NAMED('uppercase', RE_IDENTIFIER), r'\s*,\s*', + OR(RE_IDENTIFIER, RE_STRING, RE_MACRO_CONCAT, RE_FUN_CALL, name='typename'), SP, + r'\)[ \t]*;?[ \t]*\n') + + def required_identifiers(self) -> Iterable[RequiredIdentifier]: + yield RequiredIdentifier('include', '"qom/object.h"') + yield RequiredIdentifier('constant', self.group('typename')) + yield RequiredIdentifier('type', self.group('instancetype')) + +class DeclareInterfaceChecker(TypeCheckerDeclaration): + """DECLARE_INTERFACE_CHECKER use""" + regexp = S(r'^[ \t]*DECLARE_INTERFACE_CHECKER\s*\(\s*', + NAMED('instancetype', RE_TYPE), r'\s*,\s*', + NAMED('uppercase', RE_IDENTIFIER), r'\s*,\s*', + OR(RE_IDENTIFIER, RE_STRING, RE_MACRO_CONCAT, RE_FUN_CALL, name='typename'), SP, + r'\)[ \t]*;?[ \t]*\n') + + def required_identifiers(self) -> Iterable[RequiredIdentifier]: + yield RequiredIdentifier('include', '"qom/object.h"') + yield RequiredIdentifier('constant', self.group('typename')) + yield RequiredIdentifier('type', self.group('instancetype')) + +class DeclareInstanceType(TypeDeclaration): + """DECLARE_INSTANCE_TYPE use""" + regexp = S(r'^[ \t]*DECLARE_INSTANCE_TYPE\s*\(\s*', + NAMED('uppercase', RE_IDENTIFIER), r'\s*,\s*', + NAMED('instancetype', RE_TYPE), SP, + r'\)[ \t]*;?[ \t]*\n') + + def required_identifiers(self) -> Iterable[RequiredIdentifier]: + yield RequiredIdentifier('include', '"qom/object.h"') + yield RequiredIdentifier('type', self.group('instancetype')) + +class DeclareClassType(TypeDeclaration): + """DECLARE_CLASS_TYPE use""" + regexp = S(r'^[ \t]*DECLARE_CLASS_TYPE\s*\(\s*', + NAMED('uppercase', RE_IDENTIFIER), r'\s*,\s*', + NAMED('classtype', RE_TYPE), SP, + r'\)[ \t]*;?[ \t]*\n') + + def required_identifiers(self) -> Iterable[RequiredIdentifier]: + yield RequiredIdentifier('include', '"qom/object.h"') + yield RequiredIdentifier('type', self.group('classtype')) + + + +class DeclareClassCheckers(TypeCheckerDeclaration): + """DECLARE_CLASS_CHECKER use""" + regexp = S(r'^[ \t]*DECLARE_CLASS_CHECKERS\s*\(\s*', + NAMED('classtype', RE_TYPE), r'\s*,\s*', + NAMED('uppercase', RE_IDENTIFIER), r'\s*,\s*', + OR(RE_IDENTIFIER, RE_STRING, RE_MACRO_CONCAT, RE_FUN_CALL, name='typename'), SP, + r'\)[ \t]*;?[ \t]*\n') + + def required_identifiers(self) -> Iterable[RequiredIdentifier]: + yield RequiredIdentifier('include', '"qom/object.h"') + yield RequiredIdentifier('constant', self.group('typename')) + yield RequiredIdentifier('type', self.group('classtype')) + +class DeclareObjCheckers(TypeCheckerDeclaration): + """DECLARE_OBJ_CHECKERS use""" + #TODO: detect when OBJECT_DECLARE_SIMPLE_TYPE can be used + regexp = S(r'^[ \t]*DECLARE_OBJ_CHECKERS\s*\(\s*', + NAMED('instancetype', RE_TYPE), r'\s*,\s*', + NAMED('classtype', RE_TYPE), r'\s*,\s*', + NAMED('uppercase', RE_IDENTIFIER), r'\s*,\s*', + OR(RE_IDENTIFIER, RE_STRING, RE_MACRO_CONCAT, RE_FUN_CALL, name='typename'), SP, + r'\)[ \t]*;?[ \t]*\n') + + def required_identifiers(self) -> Iterable[RequiredIdentifier]: + yield RequiredIdentifier('include', '"qom/object.h"') + yield RequiredIdentifier('constant', self.group('typename')) + yield RequiredIdentifier('type', self.group('classtype')) + yield RequiredIdentifier('type', self.group('instancetype')) + +class TypeDeclarationFixup(FileMatch): + """Common base class for code that will look at a set of type declarations""" + regexp = RE_FILE_BEGIN + def gen_patches(self) -> Iterable[Patch]: + if self.file.filename_matches('qom/object.h'): + self.debug("skipping object.h") + return + + # group checkers by uppercase name: + decl_types: List[Type[TypeDeclaration]] = [DeclareInstanceChecker, DeclareInstanceType, + DeclareClassCheckers, DeclareClassType, + DeclareObjCheckers] + checker_dict: Dict[str, List[TypeDeclaration]] = {} + for t in decl_types: + for m in self.file.matches_of_type(t): + checker_dict.setdefault(m.group('uppercase'), []).append(m) + self.debug("checker_dict: %r", checker_dict) + for uppercase,checkers in checker_dict.items(): + fields = ('instancetype', 'classtype', 'uppercase', 'typename') + fvalues = dict((field, set(getattr(m, field) for m in checkers + if getattr(m, field, None) is not None)) + for field in fields) + for field,values in fvalues.items(): + if len(values) > 1: + for c in checkers: + c.warn("%s mismatch (%s)", field, ' '.join(values)) + return + + field_dict = dict((f, v.pop() if v else None) for f,v in fvalues.items()) + yield from self.gen_patches_for_type(uppercase, checkers, field_dict) + + def find_conflicts(self, uppercase: str, checkers: List[TypeDeclaration]) -> bool: + """Look for conflicting declarations that would make it unsafe to add new ones""" + conflicting: List[FileMatch] = [] + # conflicts in the same file: + conflicting.extend(chain(self.file.find_matches(DefineDirective, uppercase), + self.file.find_matches(DeclareInterfaceChecker, uppercase, 'uppercase'), + self.file.find_matches(DeclareClassType, uppercase, 'uppercase'), + self.file.find_matches(DeclareInstanceType, uppercase, 'uppercase'))) + + # conflicts in another file: + conflicting.extend(o for o in chain(self.allfiles.find_matches(DeclareInstanceChecker, uppercase, 'uppercase'), + self.allfiles.find_matches(DeclareClassCheckers, uppercase, 'uppercase'), + self.allfiles.find_matches(DeclareInterfaceChecker, uppercase, 'uppercase'), + self.allfiles.find_matches(DefineDirective, uppercase)) + if o is not None and o.file != self.file + # if both are .c files, there's no conflict at all: + and not (o.file.filename.suffix == '.c' and + self.file.filename.suffix == '.c')) + + if conflicting: + for c in checkers: + c.warn("skipping due to conflicting %s macro", uppercase) + for o in conflicting: + if o is None: + continue + o.warn("conflicting %s macro is here", uppercase) + return True + + return False + + def gen_patches_for_type(self, uppercase: str, + checkers: List[TypeDeclaration], + fields: Dict[str, Optional[str]]) -> Iterable[Patch]: + """Should be reimplemented by subclasses""" + return + yield + +class DeclareVoidTypes(TypeDeclarationFixup): + """Add DECLARE_*_TYPE(..., void) when there's no declared type""" + regexp = RE_FILE_BEGIN + def gen_patches_for_type(self, uppercase: str, + checkers: List[TypeDeclaration], + fields: Dict[str, Optional[str]]) -> Iterable[Patch]: + if self.find_conflicts(uppercase, checkers): + return + + #_,last_checker = max((m.start(), m) for m in checkers) + _,first_checker = min((m.start(), m) for m in checkers) + + if not any(m.instancetype for m in checkers): + yield first_checker.prepend(f'DECLARE_INSTANCE_TYPE({uppercase}, void)\n') + if not any(m.classtype for m in checkers): + yield first_checker.prepend(f'DECLARE_CLASS_TYPE({uppercase}, void)\n') + + #if not all(len(v) == 1 for v in fvalues.values()): + # return + # + #final_values = dict((field, values.pop()) + # for field,values in fvalues.items()) + #s = (f"DECLARE_OBJ_CHECKERS({final_values['instancetype']}, {final_values['classtype']},\n"+ + # f" {final_values['uppercase']}, {final_values['typename']})\n") + #for c in checkers: + # yield c.make_removal_patch() + #yield last_checker.append(s) + + +class AddDeclareTypeName(TypeDeclarationFixup): + """Add DECLARE_TYPE_NAME declarations if necessary""" + def gen_patches_for_type(self, uppercase: str, + checkers: List[TypeDeclaration], + fields: Dict[str, Optional[str]]) -> Iterable[Patch]: + typename = fields.get('typename') + if typename is None: + self.warn("typename unavailable") + return + if typename == f'TYPE_{uppercase}': + self.info("already using TYPE_%s as type name", uppercase) + return + if self.file.find_match(DeclareTypeName, uppercase, 'uppercase'): + self.info("type name for %s already declared", uppercase) + return + _,first_checker = min((m.start(), m) for m in checkers) + s = f'DECLARE_TYPE_NAME({uppercase}, {typename})\n' + yield first_checker.prepend(s) + +class TrivialClassStruct(FileMatch): + """Trivial class struct""" + regexp = S(r'^[ \t]*struct\s*', NAMED('name', RE_IDENTIFIER), + r'\s*{\s*', NAMED('parent_struct', RE_IDENTIFIER), r'\s*parent(_class)?\s*;\s*};\n') + +class DeclareTypeName(FileMatch): + """DECLARE_TYPE_NAME usage""" + regexp = S(r'^[ \t]*DECLARE_TYPE_NAME\s*\(', + NAMED('uppercase', RE_IDENTIFIER), r'\s*,\s*', + OR(RE_IDENTIFIER, RE_STRING, RE_MACRO_CONCAT, RE_FUN_CALL, name='typename'), + r'\s*\);?[ \t]*\n') + +class ObjectDeclareType(TypeCheckerDeclaration): + """OBJECT_DECLARE_TYPE usage + Will be replaced with OBJECT_DECLARE_SIMPLE_TYPE if possible + """ + regexp = S(r'^[ \t]*OBJECT_DECLARE_TYPE\s*\(', + NAMED('instancetype', RE_TYPE), r'\s*,\s*', + NAMED('classtype', RE_TYPE), r'\s*,\s*', + NAMED('uppercase', RE_IDENTIFIER), SP, + r'\)[ \t]*;?[ \t]*\n') + + def gen_patches(self): + DBG("groups: %r", self.match.groupdict()) + trivial_struct = self.file.find_match(TrivialClassStruct, self.group('classtype')) + if trivial_struct: + d = self.match.groupdict().copy() + d['parent_struct'] = trivial_struct.group("parent_struct") + yield trivial_struct.make_removal_patch() + c = ("OBJECT_DECLARE_SIMPLE_TYPE(%(instancetype)s, %(lowercase)s,\n" + " %(uppercase)s, %(parent_struct)s)\n" % d) + yield self.make_patch(c) + +class ObjectDeclareSimpleType(TypeCheckerDeclaration): + """OBJECT_DECLARE_SIMPLE_TYPE usage""" + regexp = S(r'^[ \t]*OBJECT_DECLARE_SIMPLE_TYPE\s*\(', + NAMED('instancetype', RE_TYPE), r'\s*,\s*', + NAMED('uppercase', RE_IDENTIFIER), SP, + r'\)[ \t]*;?[ \t]*\n') + +class OldStyleObjectDeclareSimpleType(TypeCheckerDeclaration): + """OBJECT_DECLARE_SIMPLE_TYPE usage (old API)""" + regexp = S(r'^[ \t]*OBJECT_DECLARE_SIMPLE_TYPE\s*\(', + NAMED('instancetype', RE_TYPE), r'\s*,\s*', + NAMED('lowercase', RE_IDENTIFIER), r'\s*,\s*', + NAMED('uppercase', RE_IDENTIFIER), r'\s*,\s*', + NAMED('parent_classtype', RE_TYPE), SP, + r'\)[ \t]*;?[ \t]*\n') + + @property + def classtype(self) -> Optional[str]: + instancetype = self.instancetype + assert instancetype + return f"{instancetype}Class" + +def find_typename_uppercase(files: FileList, typename: str) -> Optional[str]: + """Try to find what's the right MODULE_OBJ_NAME for a given type name""" + decl = files.find_match(DeclareTypeName, name=typename, group='typename') + if decl: + return decl.group('uppercase') + if typename.startswith('TYPE_'): + return typename[len('TYPE_'):] + return None + +def find_type_checkers(files:FileList, name:str, group:str='uppercase') -> Iterable[TypeCheckerDeclaration]: + """Find usage of DECLARE*CHECKER macro""" + c: Type[TypeCheckerDeclaration] + for c in (DeclareInstanceChecker, DeclareClassCheckers, DeclareObjCheckers, ObjectDeclareType, ObjectDeclareSimpleType): + yield from files.find_matches(c, name=name, group=group) + +class Include(FileMatch): + """#include directive""" + regexp = RE_INCLUDE + def provided_identifiers(self) -> Iterable[RequiredIdentifier]: + yield RequiredIdentifier('include', self.group('includepath')) + +class InitialIncludes(FileMatch): + """Initial #include block""" + regexp = S(RE_FILE_BEGIN, + M(SP, RE_COMMENTS, + r'^[ \t]*#[ \t]*ifndef[ \t]+', RE_IDENTIFIER, r'[ \t]*\n', + n='?', name='ifndef_block'), + M(SP, RE_COMMENTS, + OR(RE_INCLUDE, RE_SIMPLEDEFINE), + n='*', name='includes')) + +class SymbolUserList(NamedTuple): + definitions: List[FileMatch] + users: List[FileMatch] + +class MoveSymbols(FileMatch): + """Handle missing symbols + - Move typedefs and defines when necessary + - Add missing #include lines when necessary + """ + regexp = RE_FILE_BEGIN + + def gen_patches(self) -> Iterator[Patch]: + if self.file.filename_matches('qom/object.h'): + self.debug("skipping object.h") + return + + index: Dict[RequiredIdentifier, SymbolUserList] = {} + definition_classes = [SimpleTypedefMatch, FullStructTypedefMatch, ConstantDefine, Include] + user_classes = [TypeCheckMacro, DeclareObjCheckers, DeclareInstanceChecker, DeclareClassCheckers, InterfaceCheckMacro] + + # first we scan for all symbol definitions and usage: + for dc in definition_classes: + defs = self.file.matches_of_type(dc) + for d in defs: + DBG("scanning %r", d) + for i in d.provided_identifiers(): + index.setdefault(i, SymbolUserList([], [])).definitions.append(d) + DBG("index: %r", list(index.keys())) + for uc in user_classes: + users = self.file.matches_of_type(uc) + for u in users: + for i in u.required_identifiers(): + index.setdefault(i, SymbolUserList([], [])).users.append(u) + + # validate all symbols: + for i,ul in index.items(): + if not ul.users: + # unused symbol + continue + + # symbol not defined + if len(ul.definitions) == 0: + if i.type == 'include': + includes, = self.file.matches_of_type(InitialIncludes) + #FIXME: don't do this if we're already inside qom/object.h + yield includes.append(f'#include {i.name}\n') + else: + u.warn("definition of %s %s not found in file", i.type, i.name) + continue + + # symbol defined twice: + if len(ul.definitions) > 1: + ul.definitions[1].warn("%s defined twice", i.name) + ul.definitions[0].warn("previously defined here") + continue + + # symbol defined. check if all users are after its definition: + assert len(ul.definitions) == 1 + definition = ul.definitions[0] + DBG("handling repositioning of %r", definition) + earliest = min(ul.users, key=lambda u: u.start()) + if earliest.start() > definition.start(): + DBG("%r is OK", definition) + continue + + DBG("%r needs to be moved", definition) + if isinstance(definition, SimpleTypedefMatch) \ + or isinstance(definition, ConstantDefine): + # simple typedef or define can be moved directly: + yield definition.make_removal_patch() + yield earliest.prepend(definition.group(0)) + elif isinstance(definition, FullStructTypedefMatch) \ + and definition.group('structname'): + # full struct typedef is more complex: we need to remove + # the typedef + yield from definition.move_typedef(earliest.start()) + else: + definition.warn("definition of %s %s needs to be moved earlier in the file", i.type, i.name) + earliest.warn("definition of %s %s is used here", i.type, i.name) + + +class EmptyPreprocessorConditional(FileMatch): + """Delete empty preprocessor conditionals""" + regexp = r'^[ \t]*#(if|ifdef)[ \t].*\n+[ \t]*#endif[ \t]*\n' + def gen_patches(self) -> Iterable[Patch]: + yield self.make_removal_patch() diff --git a/scripts/codeconverter/codeconverter/qom_type_info.py b/scripts/codeconverter/codeconverter/qom_type_info.py new file mode 100644 index 000000000..255cb5992 --- /dev/null +++ b/scripts/codeconverter/codeconverter/qom_type_info.py @@ -0,0 +1,969 @@ +# Copyright (C) 2020 Red Hat Inc. +# +# Authors: +# Eduardo Habkost <ehabkost@redhat.com> +# +# This work is licensed under the terms of the GNU GPL, version 2. See +# the COPYING file in the top-level directory. +import re +from .regexps import * +from .patching import * +from .utils import * +from .qom_macros import * + +TI_FIELDS = [ 'name', 'parent', 'abstract', 'interfaces', + 'instance_size', 'instance_init', 'instance_post_init', 'instance_finalize', + 'class_size', 'class_init', 'class_base_init', 'class_data'] + +RE_TI_FIELD_NAME = OR(*TI_FIELDS) + +RE_TI_FIELD_INIT = S(r'[ \t]*', NAMED('comments', RE_COMMENTS), + r'\.', NAMED('field', RE_TI_FIELD_NAME), r'\s*=\s*', + NAMED('value', RE_EXPRESSION), r'[ \t]*,?[ \t]*\n') +RE_TI_FIELDS = M(RE_TI_FIELD_INIT) + +RE_TYPEINFO_START = S(r'^[ \t]*', M(r'(static|const)\s+', name='modifiers'), r'TypeInfo\s+', + NAMED('name', RE_IDENTIFIER), r'\s*=\s*{[ \t]*\n') + +ParsedArray = List[str] +ParsedInitializerValue = Union[str, ParsedArray] +class InitializerValue(NamedTuple): + raw: str + parsed: Optional[ParsedInitializerValue] + match: Optional[Match] + +class ArrayItem(FileMatch): + regexp = RE_ARRAY_ITEM + +class ArrayInitializer(FileMatch): + regexp = RE_ARRAY + + def parsed(self) -> ParsedArray: + #DBG('parse_array: %r', m.group(0)) + return [m.group('arrayitem') for m in self.group_finditer(ArrayItem, 'arrayitems')] + +class FieldInitializer(FileMatch): + regexp = RE_TI_FIELD_INIT + + @property + def raw(self) -> str: + return self.group('value') + + @property + def parsed(self) -> ParsedInitializerValue: + parsed: ParsedInitializerValue = self.raw + #DBG("parse_initializer_value: %r", s) + array = self.try_group_match(ArrayInitializer, 'value') + if array: + assert isinstance(array, ArrayInitializer) + return array.parsed() + return parsed + +TypeInfoInitializers = Dict[str, FieldInitializer] + +class TypeDefinition(FileMatch): + """ + Common base class for type definitions (TypeInfo variables or OBJECT_DEFINE* macros) + """ + @property + def instancetype(self) -> Optional[str]: + return self.group('instancetype') + + @property + def classtype(self) -> Optional[str]: + return self.group('classtype') + + @property + def uppercase(self) -> Optional[str]: + return self.group('uppercase') + + @property + def parent_uppercase(self) -> str: + return self.group('parent_uppercase') + + @property + def initializers(self) -> Optional[TypeInfoInitializers]: + if getattr(self, '_inititalizers', None): + self._initializers: TypeInfoInitializers + return self._initializers + fields = self.group('fields') + if fields is None: + return None + d = dict((fm.group('field'), fm) + for fm in self.group_finditer(FieldInitializer, 'fields')) + self._initializers = d # type: ignore + return self._initializers + + +class TypeInfoVar(TypeDefinition): + """TypeInfo variable declaration with initializer""" + regexp = S(NAMED('begin', RE_TYPEINFO_START), + M(NAMED('fields', RE_TI_FIELDS), + NAMED('endcomments', SP, RE_COMMENTS), + NAMED('end', r'};?\n'), + n='?', name='fullspec')) + + def is_static(self) -> bool: + return 'static' in self.group('modifiers') + + def is_const(self) -> bool: + return 'const' in self.group('modifiers') + + def is_full(self) -> bool: + return bool(self.group('fullspec')) + + def get_initializers(self) -> TypeInfoInitializers: + """Helper for code that needs to deal with missing initializer info""" + if self.initializers is None: + return {} + return self.initializers + + def get_raw_initializer_value(self, field: str, default: str = '') -> str: + initializers = self.get_initializers() + if field in initializers: + return initializers[field].raw + else: + return default + + @property + def typename(self) -> Optional[str]: + return self.get_raw_initializer_value('name') + + @property + def uppercase(self) -> Optional[str]: + typename = self.typename + if not typename: + return None + if not typename.startswith('TYPE_'): + return None + return typename[len('TYPE_'):] + + @property + def classtype(self) -> Optional[str]: + class_size = self.get_raw_initializer_value('class_size') + if not class_size: + return None + m = re.fullmatch(RE_SIZEOF, class_size) + if not m: + return None + return m.group('sizeoftype') + + @property + def instancetype(self) -> Optional[str]: + instance_size = self.get_raw_initializer_value('instance_size') + if not instance_size: + return None + m = re.fullmatch(RE_SIZEOF, instance_size) + if not m: + return None + return m.group('sizeoftype') + + + #def extract_identifiers(self) -> Optional[TypeIdentifiers]: + # """Try to extract identifiers from names being used""" + # DBG("extracting idenfiers from %s", self.name) + #uppercase = None + #if typename and re.fullmatch(RE_IDENTIFIER, typename) and typename.startswith("TYPE_"): + # uppercase = typename[len('TYPE_'):] + #lowercase = None + #funcs = set() + #prefixes = set() + #for field,suffix in [('instance_init', '_init'), + # ('instance_finalize', '_finalize'), + # ('class_init', '_class_init')]: + # if field not in values: + # continue + # func = values[field].raw + # funcs.add(func) + # if func.endswith(suffix): + # prefixes.add(func[:-len(suffix)]) + # else: + # self.warn("function name %s doesn't have expected %s suffix", + # func, suffix) + #if len(prefixes) == 1: + # lowercase = prefixes.pop() + #elif len(prefixes) > 1: + # self.warn("inconsistent function names: %s", ' '.join(funcs)) + + #.parent = TYPE_##PARENT_MODULE_OBJ_NAME, \ + #return TypeIdentifiers(typename=typename, + # uppercase=uppercase, lowercase=lowercase, + # instancetype=instancetype, classtype=classtype) + + def append_field(self, field: str, value: str) -> Patch: + """Generate patch appending a field initializer""" + content = f' .{field} = {value},\n' + fm = self.group_match('fields') + assert fm + return fm.append(content) + + def patch_field(self, field: str, replacement: str) -> Patch: + """Generate patch replacing a field initializer""" + initializers = self.initializers + assert initializers + value = initializers.get(field) + assert value + return value.make_patch(replacement) + + def remove_field(self, field: str) -> Iterable[Patch]: + initializers = self.initializers + assert initializers + if field in initializers: + yield self.patch_field(field, '') + + def remove_fields(self, *fields: str) -> Iterable[Patch]: + for f in fields: + yield from self.remove_field(f) + + def patch_field_value(self, field: str, replacement: str) -> Patch: + """Replace just the value of a field initializer""" + initializers = self.initializers + assert initializers + value = initializers.get(field) + assert value + vm = value.group_match('value') + assert vm + return vm.make_patch(replacement) + + +class RemoveRedundantClassSize(TypeInfoVar): + """Remove class_size when using OBJECT_DECLARE_SIMPLE_TYPE""" + def gen_patches(self) -> Iterable[Patch]: + initializers = self.initializers + if initializers is None: + return + if 'class_size' not in initializers: + return + + self.debug("Handling %s", self.name) + m = re.fullmatch(RE_SIZEOF, initializers['class_size'].raw) + if not m: + self.warn("%s class_size is not sizeof?", self.name) + return + classtype = m.group('sizeoftype') + if not classtype.endswith('Class'): + self.warn("%s class size type (%s) is not *Class?", self.name, classtype) + return + self.debug("classtype is %s", classtype) + instancetype = classtype[:-len('Class')] + self.debug("intanceypte is %s", instancetype) + self.debug("searching for simpletype declaration using %s as InstanceType", instancetype) + decl = self.allfiles.find_match(OldStyleObjectDeclareSimpleType, + instancetype, 'instancetype') + if not decl: + self.debug("No simpletype declaration found for %s", instancetype) + return + self.debug("Found simple type declaration") + decl.debug("declaration is here") + yield from self.remove_field('class_size') + +class RemoveDeclareSimpleTypeArg(OldStyleObjectDeclareSimpleType): + """Remove class_size when using OBJECT_DECLARE_SIMPLE_TYPE""" + def gen_patches(self) -> Iterable[Patch]: + c = (f'OBJECT_DECLARE_SIMPLE_TYPE({self.group("instancetype")}, {self.group("lowercase")},\n' + f' {self.group("uppercase")})\n') + yield self.make_patch(c) + +class UseDeclareTypeExtended(TypeInfoVar): + """Replace TypeInfo variable with OBJECT_DEFINE_TYPE_EXTENDED""" + def gen_patches(self) -> Iterable[Patch]: + # this will just ensure the caches for find_match() and matches_for_type() + # will be loaded in advance: + find_type_checkers(self.allfiles, 'xxxxxxxxxxxxxxxxx') + + if not self.is_static(): + self.info("Skipping non-static TypeInfo variable") + return + + type_info_macro = self.file.find_match(TypeInfoMacro, self.name) + if not type_info_macro: + self.warn("TYPE_INFO(%s) line not found", self.name) + return + + values = self.initializers + if values is None: + return + if 'name' not in values: + self.warn("name not set in TypeInfo variable %s", self.name) + return + + typename = values['name'].raw + + if 'parent' not in values: + self.warn("parent not set in TypeInfo variable %s", self.name) + return + parent_typename = values['parent'].raw + + instancetype = None + if 'instance_size' in values: + m = re.fullmatch(RE_SIZEOF, values['instance_size'].raw) + if m: + instancetype = m.group('sizeoftype') + else: + self.warn("can't extract instance type in TypeInfo variable %s", self.name) + self.warn("instance_size is set to: %r", values['instance_size'].raw) + return + + classtype = None + if 'class_size' in values: + m = re.fullmatch(RE_SIZEOF, values['class_size'].raw) + if m: + classtype = m.group('sizeoftype') + else: + self.warn("can't extract class type in TypeInfo variable %s", self.name) + self.warn("class_size is set to: %r", values['class_size'].raw) + return + + #for t in (typename, parent_typename): + # if not re.fullmatch(RE_IDENTIFIER, t): + # self.info("type name is not a macro/constant") + # if instancetype or classtype: + # self.warn("macro/constant type name is required for instance/class type") + # if not self.file.force: + # return + + # Now, the challenge is to find out the right MODULE_OBJ_NAME for the + # type and for the parent type + self.info("TypeInfo variable for %s is here", typename) + uppercase = find_typename_uppercase(self.allfiles, typename) + if not uppercase: + self.info("Can't find right uppercase name for %s", typename) + if instancetype or classtype: + self.warn("Can't find right uppercase name for %s", typename) + self.warn("This will make type validation difficult in the future") + return + + parent_uppercase = find_typename_uppercase(self.allfiles, parent_typename) + if not parent_uppercase: + self.info("Can't find right uppercase name for parent type (%s)", parent_typename) + if instancetype or classtype: + self.warn("Can't find right uppercase name for parent type (%s)", parent_typename) + self.warn("This will make type validation difficult in the future") + return + + ok = True + + #checkers: List[TypeCheckerDeclaration] = list(find_type_checkers(self.allfiles, uppercase)) + #for c in checkers: + # c.info("instance type checker declaration (%s) is here", c.group('uppercase')) + #if not checkers: + # self.info("No type checkers declared for %s", uppercase) + # if instancetype or classtype: + # self.warn("Can't find where type checkers for %s (%s) are declared. We will need them to validate sizes of %s", + # typename, uppercase, self.name) + + if not instancetype: + instancetype = 'void' + if not classtype: + classtype = 'void' + + #checker_instancetypes = set(c.instancetype for c in checkers + # if c.instancetype is not None) + #if len(checker_instancetypes) > 1: + # self.warn("ambiguous set of type checkers") + # for c in checkers: + # c.warn("instancetype is %s here", c.instancetype) + # ok = False + #elif len(checker_instancetypes) == 1: + # checker_instancetype = checker_instancetypes.pop() + # DBG("checker instance type: %r", checker_instancetype) + # if instancetype != checker_instancetype: + # self.warn("type at instance_size is %r. Should instance_size be set to sizeof(%s) ?", + # instancetype, checker_instancetype) + # ok = False + #else: + # if instancetype != 'void': + # self.warn("instance type checker for %s (%s) not found", typename, instancetype) + # ok = False + + #checker_classtypes = set(c.classtype for c in checkers + # if c.classtype is not None) + #if len(checker_classtypes) > 1: + # self.warn("ambiguous set of type checkers") + # for c in checkers: + # c.warn("classtype is %s here", c.classtype) + # ok = False + #elif len(checker_classtypes) == 1: + # checker_classtype = checker_classtypes.pop() + # DBG("checker class type: %r", checker_classtype) + # if classtype != checker_classtype: + # self.warn("type at class_size is %r. Should class_size be set to sizeof(%s) ?", + # classtype, checker_classtype) + # ok = False + #else: + # if classtype != 'void': + # self.warn("class type checker for %s (%s) not found", typename, classtype) + # ok = False + + #if not ok: + # for c in checkers: + # c.warn("Type checker declaration for %s (%s) is here", + # typename, type(c).__name__) + # return + + #if parent_decl is None: + # self.warn("Can't find where parent type %s is declared", parent_typename) + + #yield self.prepend(f'DECLARE_TYPE_NAME({uppercase}, {typename})\n') + #if not instancetype: + # yield self.prepend(f'DECLARE_INSTANCE_TYPE({uppercase}, void)\n') + #if not classtype: + # yield self.prepend(f'DECLARE_CLASS_TYPE({uppercase}, void)\n') + self.info("%s can be patched!", self.name) + replaced_fields = ['name', 'parent', 'instance_size', 'class_size'] + begin = self.group_match('begin') + newbegin = f'OBJECT_DEFINE_TYPE_EXTENDED({self.name},\n' + newbegin += f' {instancetype}, {classtype},\n' + newbegin += f' {uppercase}, {parent_uppercase}' + if set(values.keys()) - set(replaced_fields): + newbegin += ',\n' + yield begin.make_patch(newbegin) + yield from self.remove_fields(*replaced_fields) + end = self.group_match('end') + yield end.make_patch(')\n') + yield type_info_macro.make_removal_patch() + +class ObjectDefineTypeExtended(TypeDefinition): + """OBJECT_DEFINE_TYPE_EXTENDED usage""" + regexp = S(r'^[ \t]*OBJECT_DEFINE_TYPE_EXTENDED\s*\(\s*', + NAMED('name', RE_IDENTIFIER), r'\s*,\s*', + NAMED('instancetype', RE_IDENTIFIER), r'\s*,\s*', + NAMED('classtype', RE_IDENTIFIER), r'\s*,\s*', + NAMED('uppercase', RE_IDENTIFIER), r'\s*,\s*', + NAMED('parent_uppercase', RE_IDENTIFIER), + M(r',\s*\n', + NAMED('fields', RE_TI_FIELDS), + n='?'), + r'\s*\);?\n?') + +class ObjectDefineType(TypeDefinition): + """OBJECT_DEFINE_TYPE usage""" + regexp = S(r'^[ \t]*OBJECT_DEFINE_TYPE\s*\(\s*', + NAMED('lowercase', RE_IDENTIFIER), r'\s*,\s*', + NAMED('uppercase', RE_IDENTIFIER), r'\s*,\s*', + NAMED('parent_uppercase', RE_IDENTIFIER), + M(r',\s*\n', + NAMED('fields', RE_TI_FIELDS), + n='?'), + r'\s*\);?\n?') + +def find_type_definitions(files: FileList, uppercase: str) -> Iterable[TypeDefinition]: + types: List[Type[TypeDefinition]] = [TypeInfoVar, ObjectDefineType, ObjectDefineTypeExtended] + for t in types: + for m in files.matches_of_type(t): + m.debug("uppercase: %s", m.uppercase) + yield from (m for t in types + for m in files.matches_of_type(t) + if m.uppercase == uppercase) + +class AddDeclareVoidClassType(TypeDeclarationFixup): + """Will add DECLARE_CLASS_TYPE(..., void) if possible""" + def gen_patches_for_type(self, uppercase: str, + checkers: List[TypeDeclaration], + fields: Dict[str, Optional[str]]) -> Iterable[Patch]: + defs = list(find_type_definitions(self.allfiles, uppercase)) + if len(defs) > 1: + self.warn("multiple definitions for %s", uppercase) + for d in defs: + d.warn("definition found here") + return + elif len(defs) == 0: + self.warn("type definition for %s not found", uppercase) + return + d = defs[0] + if d.classtype is None: + d.info("definition for %s has classtype, skipping", uppercase) + return + class_type_checkers = [c for c in checkers + if c.classtype is not None] + if class_type_checkers: + for c in class_type_checkers: + c.warn("class type checker for %s is present here", uppercase) + return + + _,last_checker = max((m.start(), m) for m in checkers) + s = f'DECLARE_CLASS_TYPE({uppercase}, void)\n' + yield last_checker.append(s) + +class AddDeclareVoidInstanceType(FileMatch): + """Will add DECLARE_INSTANCE_TYPE(..., void) if possible""" + regexp = S(r'^[ \t]*#[ \t]*define', CPP_SPACE, + NAMED('name', r'TYPE_[a-zA-Z0-9_]+\b'), + CPP_SPACE, r'.*\n') + + def gen_patches(self) -> Iterable[Patch]: + assert self.name.startswith('TYPE_') + uppercase = self.name[len('TYPE_'):] + defs = list(find_type_definitions(self.allfiles, uppercase)) + if len(defs) > 1: + self.warn("multiple definitions for %s", uppercase) + for d in defs: + d.warn("definition found here") + return + elif len(defs) == 0: + self.warn("type definition for %s not found", uppercase) + return + d = defs[0] + instancetype = d.instancetype + if instancetype is not None and instancetype != 'void': + return + + instance_checkers = [c for c in find_type_checkers(self.allfiles, uppercase) + if c.instancetype] + if instance_checkers: + d.warn("instance type checker for %s already declared", uppercase) + for c in instance_checkers: + c.warn("instance checker for %s is here", uppercase) + return + + s = f'DECLARE_INSTANCE_TYPE({uppercase}, void)\n' + yield self.append(s) + +class AddObjectDeclareType(DeclareObjCheckers): + """Will add OBJECT_DECLARE_TYPE(...) if possible""" + def gen_patches(self) -> Iterable[Patch]: + uppercase = self.uppercase + typename = self.group('typename') + instancetype = self.group('instancetype') + classtype = self.group('classtype') + + if typename != f'TYPE_{uppercase}': + self.warn("type name mismatch: %s vs %s", typename, uppercase) + return + + typedefs = [(t,self.allfiles.find_matches(SimpleTypedefMatch, t)) + for t in (instancetype, classtype)] + for t,tds in typedefs: + if not tds: + self.warn("typedef %s not found", t) + return + for td in tds: + td_type = td.group('typedef_type') + if td_type != f'struct {t}': + self.warn("typedef mismatch: %s is defined as %s", t, td_type) + td.warn("typedef is here") + return + + # look for reuse of same struct type + other_instance_checkers = [c for c in find_type_checkers(self.allfiles, instancetype, 'instancetype') + if c.uppercase != uppercase] + if other_instance_checkers: + self.warn("typedef %s is being reused", instancetype) + for ic in other_instance_checkers: + ic.warn("%s is reused here", instancetype) + if not self.file.force: + return + + decl_types: List[Type[TypeDeclaration]] = [DeclareClassCheckers, DeclareObjCheckers] + class_decls = [m for t in decl_types + for m in self.allfiles.find_matches(t, uppercase, 'uppercase')] + + defs = list(find_type_definitions(self.allfiles, uppercase)) + if len(defs) > 1: + self.warn("multiple definitions for %s", uppercase) + for d in defs: + d.warn("definition found here") + if not self.file.force: + return + elif len(defs) == 0: + self.warn("type definition for %s not found", uppercase) + if not self.file.force: + return + else: + d = defs[0] + if d.instancetype != instancetype: + self.warn("mismatching instance type for %s (%s)", uppercase, instancetype) + d.warn("instance type declared here (%s)", d.instancetype) + if not self.file.force: + return + if d.classtype != classtype: + self.warn("mismatching class type for %s (%s)", uppercase, classtype) + d.warn("class type declared here (%s)", d.classtype) + if not self.file.force: + return + + assert self.file.original_content + for t,tds in typedefs: + assert tds + for td in tds: + if td.file is not self.file: + continue + + # delete typedefs that are truly redundant: + # 1) defined after DECLARE_OBJ_CHECKERS + if td.start() > self.start(): + yield td.make_removal_patch() + # 2) defined before DECLARE_OBJ_CHECKERS, but unused + elif not re.search(r'\b'+t+r'\b', self.file.original_content[td.end():self.start()]): + yield td.make_removal_patch() + + c = (f'OBJECT_DECLARE_TYPE({instancetype}, {classtype}, {uppercase})\n') + yield self.make_patch(c) + +class AddObjectDeclareSimpleType(DeclareInstanceChecker): + """Will add OBJECT_DECLARE_SIMPLE_TYPE(...) if possible""" + def gen_patches(self) -> Iterable[Patch]: + uppercase = self.uppercase + typename = self.group('typename') + instancetype = self.group('instancetype') + + if typename != f'TYPE_{uppercase}': + self.warn("type name mismatch: %s vs %s", typename, uppercase) + return + + typedefs = [(t,self.allfiles.find_matches(SimpleTypedefMatch, t)) + for t in (instancetype,)] + for t,tds in typedefs: + if not tds: + self.warn("typedef %s not found", t) + return + for td in tds: + td_type = td.group('typedef_type') + if td_type != f'struct {t}': + self.warn("typedef mismatch: %s is defined as %s", t, td_type) + td.warn("typedef is here") + return + + # look for reuse of same struct type + other_instance_checkers = [c for c in find_type_checkers(self.allfiles, instancetype, 'instancetype') + if c.uppercase != uppercase] + if other_instance_checkers: + self.warn("typedef %s is being reused", instancetype) + for ic in other_instance_checkers: + ic.warn("%s is reused here", instancetype) + if not self.file.force: + return + + decl_types: List[Type[TypeDeclaration]] = [DeclareClassCheckers, DeclareObjCheckers] + class_decls = [m for t in decl_types + for m in self.allfiles.find_matches(t, uppercase, 'uppercase')] + if class_decls: + self.warn("class type declared for %s", uppercase) + for cd in class_decls: + cd.warn("class declaration found here") + return + + defs = list(find_type_definitions(self.allfiles, uppercase)) + if len(defs) > 1: + self.warn("multiple definitions for %s", uppercase) + for d in defs: + d.warn("definition found here") + if not self.file.force: + return + elif len(defs) == 0: + self.warn("type definition for %s not found", uppercase) + if not self.file.force: + return + else: + d = defs[0] + if d.instancetype != instancetype: + self.warn("mismatching instance type for %s (%s)", uppercase, instancetype) + d.warn("instance type declared here (%s)", d.instancetype) + if not self.file.force: + return + if d.classtype: + self.warn("class type set for %s", uppercase) + d.warn("class type declared here") + if not self.file.force: + return + + assert self.file.original_content + for t,tds in typedefs: + assert tds + for td in tds: + if td.file is not self.file: + continue + + # delete typedefs that are truly redundant: + # 1) defined after DECLARE_OBJ_CHECKERS + if td.start() > self.start(): + yield td.make_removal_patch() + # 2) defined before DECLARE_OBJ_CHECKERS, but unused + elif not re.search(r'\b'+t+r'\b', self.file.original_content[td.end():self.start()]): + yield td.make_removal_patch() + + c = (f'OBJECT_DECLARE_SIMPLE_TYPE({instancetype}, {uppercase})\n') + yield self.make_patch(c) + + +class TypeInfoStringName(TypeInfoVar): + """Replace hardcoded type names with TYPE_ constant""" + def gen_patches(self) -> Iterable[Patch]: + values = self.initializers + if values is None: + return + if 'name' not in values: + self.warn("name not set in TypeInfo variable %s", self.name) + return + typename = values['name'].raw + if re.fullmatch(RE_IDENTIFIER, typename): + return + + self.warn("name %s is not an identifier", typename) + #all_defines = [m for m in self.allfiles.matches_of_type(ExpressionDefine)] + #self.debug("all_defines: %r", all_defines) + constants = [m for m in self.allfiles.matches_of_type(ExpressionDefine) + if m.group('value').strip() == typename.strip()] + if not constants: + self.warn("No macro for %s found", typename) + return + if len(constants) > 1: + self.warn("I don't know which macro to use: %r", constants) + return + yield self.patch_field_value('name', constants[0].name) + +class RedundantTypeSizes(TypeInfoVar): + """Remove redundant instance_size/class_size from TypeInfo vars""" + def gen_patches(self) -> Iterable[Patch]: + values = self.initializers + if values is None: + return + if 'name' not in values: + self.warn("name not set in TypeInfo variable %s", self.name) + return + typename = values['name'].raw + if 'parent' not in values: + self.warn("parent not set in TypeInfo variable %s", self.name) + return + parent_typename = values['parent'].raw + + if 'instance_size' not in values and 'class_size' not in values: + self.debug("no need to validate %s", self.name) + return + + instance_decls = find_type_checkers(self.allfiles, typename) + if instance_decls: + self.debug("won't touch TypeInfo var that has type checkers") + return + + parent = find_type_info(self.allfiles, parent_typename) + if not parent: + self.warn("Can't find TypeInfo for %s", parent_typename) + return + + if 'instance_size' in values and parent.get_raw_initializer_value('instance_size') != values['instance_size'].raw: + self.info("instance_size mismatch") + parent.info("parent type declared here") + return + + if 'class_size' in values and parent.get_raw_initializer_value('class_size') != values['class_size'].raw: + self.info("class_size mismatch") + parent.info("parent type declared here") + return + + self.debug("will patch variable %s", self.name) + + if 'instance_size' in values: + self.debug("deleting instance_size") + yield self.patch_field('instance_size', '') + + if 'class_size' in values: + self.debug("deleting class_size") + yield self.patch_field('class_size', '') + + +#class TypeInfoVarInitFuncs(TypeInfoVar): +# """TypeInfo variable +# Will create missing init functions +# """ +# def gen_patches(self) -> Iterable[Patch]: +# values = self.initializers +# if values is None: +# self.warn("type not parsed completely: %s", self.name) +# return +# +# macro = self.file.find_match(TypeInfoVar, self.name) +# if macro is None: +# self.warn("No TYPE_INFO macro for %s", self.name) +# return +# +# ids = self.extract_identifiers() +# if ids is None: +# return +# +# DBG("identifiers extracted: %r", ids) +# fields = set(values.keys()) +# if ids.lowercase: +# if 'instance_init' not in fields: +# yield self.prepend(('static void %s_init(Object *obj)\n' +# '{\n' +# '}\n\n') % (ids.lowercase)) +# yield self.append_field('instance_init', ids.lowercase+'_init') +# +# if 'instance_finalize' not in fields: +# yield self.prepend(('static void %s_finalize(Object *obj)\n' +# '{\n' +# '}\n\n') % (ids.lowercase)) +# yield self.append_field('instance_finalize', ids.lowercase+'_finalize') +# +# +# if 'class_init' not in fields: +# yield self.prepend(('static void %s_class_init(ObjectClass *oc, void *data)\n' +# '{\n' +# '}\n\n') % (ids.lowercase)) +# yield self.append_field('class_init', ids.lowercase+'_class_init') + +class TypeInitMacro(FileMatch): + """Use of type_init(...) macro""" + regexp = S(r'^[ \t]*type_init\s*\(\s*', NAMED('name', RE_IDENTIFIER), r'\s*\);?[ \t]*\n') + +class DeleteEmptyTypeInitFunc(TypeInitMacro): + """Delete empty function declared using type_init(...)""" + def gen_patches(self) -> Iterable[Patch]: + fn = self.file.find_match(StaticVoidFunction, self.name) + DBG("function for %s: %s", self.name, fn) + if fn and fn.body == '': + yield fn.make_patch('') + yield self.make_patch('') + +class StaticVoidFunction(FileMatch): + """simple static void function + (no replacement rules) + """ + #NOTE: just like RE_FULL_STRUCT, this doesn't parse any of the body contents + # of the function. Tt will just look for "}" in the beginning of a line + regexp = S(r'static\s+void\s+', NAMED('name', RE_IDENTIFIER), r'\s*\(\s*void\s*\)\n', + r'{\n', + NAMED('body', + # acceptable inside the function body: + # - lines starting with space or tab + # - empty lines + # - preprocessor directives + OR(r'[ \t][^\n]*\n', + r'#[^\n]*\n', + r'\n', + repeat='*')), + r'};?\n') + + @property + def body(self) -> str: + return self.group('body') + + def has_preprocessor_directive(self) -> bool: + return bool(re.search(r'^[ \t]*#', self.body, re.MULTILINE)) + +def find_containing_func(m: FileMatch) -> Optional['StaticVoidFunction']: + """Return function containing this match""" + for fn in m.file.matches_of_type(StaticVoidFunction): + if fn.contains(m): + return fn + return None + +class TypeRegisterStaticCall(FileMatch): + """type_register_static() call + Will be replaced by TYPE_INFO() macro + """ + regexp = S(r'^[ \t]*', NAMED('func_name', 'type_register_static'), + r'\s*\(&\s*', NAMED('name', RE_IDENTIFIER), r'\s*\);[ \t]*\n') + +class UseTypeInfo(TypeRegisterStaticCall): + """Replace type_register_static() call with TYPE_INFO declaration""" + def gen_patches(self) -> Iterable[Patch]: + fn = find_containing_func(self) + if fn: + DBG("%r is inside %r", self, fn) + type_init = self.file.find_match(TypeInitMacro, fn.name) + if type_init is None: + self.warn("can't find type_init(%s) line", fn.name) + if not self.file.force: + return + else: + self.warn("can't identify the function where type_register_static(&%s) is called", self.name) + if not self.file.force: + return + + #if fn.has_preprocessor_directive() and not self.file.force: + # self.warn("function %s has preprocessor directives, this requires --force", fn.name) + # return + + var = self.file.find_match(TypeInfoVar, self.name) + if var is None: + self.warn("can't find TypeInfo var declaration for %s", self.name) + return + + if not var.is_full(): + self.warn("variable declaration %s wasn't parsed fully", var.name) + if not self.file.force: + return + + if fn and fn.contains(var): + self.warn("TypeInfo %s variable is inside a function", self.name) + if not self.file.force: + return + + # delete type_register_static() call: + yield self.make_patch('') + # append TYPE_REGISTER(...) after variable declaration: + yield var.append(f'TYPE_INFO({self.name})\n') + +class TypeRegisterCall(FileMatch): + """type_register_static() call""" + regexp = S(r'^[ \t]*', NAMED('func_name', 'type_register'), + r'\s*\(&\s*', NAMED('name', RE_IDENTIFIER), r'\s*\);[ \t]*\n') + +class MakeTypeRegisterStatic(TypeRegisterCall): + """Make type_register() call static if variable is static const""" + def gen_patches(self): + var = self.file.find_match(TypeInfoVar, self.name) + if var is None: + self.warn("can't find TypeInfo var declaration for %s", self.name) + return + if var.is_static() and var.is_const(): + yield self.group_match('func_name').make_patch('type_register_static') + +class MakeTypeRegisterNotStatic(TypeRegisterStaticCall): + """Make type_register() call static if variable is static const""" + def gen_patches(self): + var = self.file.find_match(TypeInfoVar, self.name) + if var is None: + self.warn("can't find TypeInfo var declaration for %s", self.name) + return + if not var.is_static() or not var.is_const(): + yield self.group_match('func_name').make_patch('type_register') + +class TypeInfoMacro(FileMatch): + """TYPE_INFO macro usage""" + regexp = S(r'^[ \t]*TYPE_INFO\s*\(\s*', NAMED('name', RE_IDENTIFIER), r'\s*\)[ \t]*;?[ \t]*\n') + +def find_type_info(files: RegexpScanner, name: str) -> Optional[TypeInfoVar]: + ti = [ti for ti in files.matches_of_type(TypeInfoVar) + if ti.get_raw_initializer_value('name') == name] + DBG("type info vars: %r", ti) + if len(ti) > 1: + DBG("multiple TypeInfo vars found for %s", name) + return None + if len(ti) == 0: + DBG("no TypeInfo var found for %s", name) + return None + return ti[0] + +class CreateClassStruct(DeclareInstanceChecker): + """Replace DECLARE_INSTANCE_CHECKER with OBJECT_DECLARE_SIMPLE_TYPE""" + def gen_patches(self) -> Iterable[Patch]: + typename = self.group('typename') + DBG("looking for TypeInfo variable for %s", typename) + var = find_type_info(self.allfiles, typename) + if var is None: + self.warn("no TypeInfo var found for %s", typename) + return + assert var.initializers + if 'class_size' in var.initializers: + self.warn("class size already set for TypeInfo %s", var.name) + return + classtype = self.group('instancetype')+'Class' + return + yield + #TODO: need to find out what's the parent class type... + #yield var.append_field('class_size', f'sizeof({classtype})') + #c = (f'OBJECT_DECLARE_SIMPLE_TYPE({instancetype}, {lowercase},\n' + # f' MODULE_OBJ_NAME, ParentClassType)\n') + #yield self.make_patch(c) + +def type_infos(file: FileInfo) -> Iterable[TypeInfoVar]: + return file.matches_of_type(TypeInfoVar) + +def full_types(file: FileInfo) -> Iterable[TypeInfoVar]: + return [t for t in type_infos(file) if t.is_full()] + +def partial_types(file: FileInfo) -> Iterable[TypeInfoVar]: + return [t for t in type_infos(file) if not t.is_full()] diff --git a/scripts/codeconverter/codeconverter/regexps.py b/scripts/codeconverter/codeconverter/regexps.py new file mode 100644 index 000000000..77993cc3b --- /dev/null +++ b/scripts/codeconverter/codeconverter/regexps.py @@ -0,0 +1,118 @@ +# Copyright (C) 2020 Red Hat Inc. +# +# Authors: +# Eduardo Habkost <ehabkost@redhat.com> +# +# This work is licensed under the terms of the GNU GPL, version 2. See +# the COPYING file in the top-level directory. +"""Helpers for creation of regular expressions""" +import re + +import logging +logger = logging.getLogger(__name__) +DBG = logger.debug +INFO = logger.info +WARN = logger.warning + +def S(*regexps) -> str: + """Just a shortcut to concatenate multiple regexps more easily""" + return ''.join(regexps) + +def P(*regexps, name=None, capture=False, repeat='') -> str: + """Just add parenthesis around regexp(s), with optional name or repeat suffix""" + s = S(*regexps) + if name: + return f'(?P<{name}>{s}){repeat}' + elif capture: + return f'({s}){repeat}' + else: + return f'(?:{s}){repeat}' + +def NAMED(name, *regexps) -> str: + """Make named group using <P<name>...) syntax + + >>> NAMED('mygroup', 'xyz', 'abc') + '(?P<mygroup>xyzabc)' + """ + return P(*regexps, name=name) + +def OR(*regexps, **kwargs) -> str: + """Build (a|b|c) regexp""" + return P('|'.join(regexps), **kwargs) + +def M(*regexps, n='*', name=None) -> str: + """Add repetition qualifier to regexp(s) + + >>> M('a', 'b') + '(?:ab)*' + >>> M('a' , 'b', n='+') + '(?:ab)+' + >>> M('a' , 'b', n='{2,3}', name='name') + '(?P<name>(?:ab){2,3})' + """ + r = P(*regexps, repeat=n) + if name: + r = NAMED(name, r) + return r + +# helper to make parenthesis optional around regexp +OPTIONAL_PARS = lambda R: OR(S(r'\(\s*', R, r'\s*\)'), R) +def test_optional_pars(): + r = OPTIONAL_PARS('abc')+'$' + assert re.match(r, 'abc') + assert re.match(r, '(abc)') + assert not re.match(r, '(abcd)') + assert not re.match(r, '(abc') + assert not re.match(r, 'abc)') + + +# this disables the MULTILINE flag, so it will match at the +# beginning of the file: +RE_FILE_BEGIN = r'(?-m:^)' + +# C primitives: + +SP = r'\s*' + +RE_COMMENT = r'//[^\n]*$|/\*([^*]|\*[^/])*\*/' +RE_COMMENTS = M(RE_COMMENT + SP) + +RE_IDENTIFIER = r'[a-zA-Z_][a-zA-Z0-9_]*(?![a-zA-Z0-9])' +RE_STRING = r'\"([^\"\\]|\\[a-z\"])*\"' +RE_NUMBER = r'[0-9]+|0x[0-9a-fA-F]+' + +# space or escaped newlines: +CPP_SPACE = OR(r'\s', r'\\\n', repeat='+') + +RE_PATH = '[a-zA-Z0-9/_.-]+' + +RE_INCLUDEPATH = OR(S(r'\"', RE_PATH, r'\"'), + S(r'<', RE_PATH, r'>')) + +RE_INCLUDE = S(r'^[ \t]*#[ \t]*include[ \t]+', NAMED('includepath', RE_INCLUDEPATH), r'[ \t]*\n') +RE_SIMPLEDEFINE = S(r'^[ \t]*#[ \t]*define[ \t]+', RE_IDENTIFIER, r'[ \t]*\n') + +RE_STRUCT_TYPE = S(r'struct\s+', RE_IDENTIFIER) +RE_TYPE = OR(RE_IDENTIFIER, RE_STRUCT_TYPE) + +RE_MACRO_CONCAT = M(S(OR(RE_IDENTIFIER, RE_STRING), SP), n='{2,}') + +RE_SIMPLE_VALUE = OR(RE_IDENTIFIER, RE_STRING, RE_NUMBER) + +RE_FUN_CALL = S(RE_IDENTIFIER, r'\s*\(\s*', RE_SIMPLE_VALUE, r'\s*\)') +RE_SIZEOF = S(r'sizeof\s*\(\s*', NAMED('sizeoftype', RE_TYPE), r'\s*\)') + +RE_ADDRESS = S(r'&\s*', RE_IDENTIFIER) + +RE_ARRAY_ITEM = S(r'{\s*', NAMED('arrayitem', M(RE_SIMPLE_VALUE, n='?')), r'\s*}\s*,?') +RE_ARRAY_CAST = S(r'\(\s*', RE_IDENTIFIER, r'\s*\[\s*\]\)') +RE_ARRAY_ITEMS = M(S(RE_ARRAY_ITEM, SP)) +RE_ARRAY = S(M(RE_ARRAY_CAST, n='?'), r'\s*{\s*', + NAMED('arrayitems', RE_ARRAY_ITEMS), + r'}') + +# NOTE: this covers a very small subset of valid expressions + +RE_EXPRESSION = OR(RE_SIZEOF, RE_FUN_CALL, RE_MACRO_CONCAT, RE_SIMPLE_VALUE, + RE_ARRAY, RE_ADDRESS) + diff --git a/scripts/codeconverter/codeconverter/test_patching.py b/scripts/codeconverter/codeconverter/test_patching.py new file mode 100644 index 000000000..71dfbd47e --- /dev/null +++ b/scripts/codeconverter/codeconverter/test_patching.py @@ -0,0 +1,104 @@ +# Copyright (C) 2020 Red Hat Inc. +# +# Authors: +# Eduardo Habkost <ehabkost@redhat.com> +# +# This work is licensed under the terms of the GNU GPL, version 2. See +# the COPYING file in the top-level directory. +from tempfile import NamedTemporaryFile +from .patching import FileInfo, FileMatch, Patch, FileList +from .regexps import * + +class BasicPattern(FileMatch): + regexp = '[abc]{3}' + + @property + def name(self): + return self.group(0) + + def replacement(self) -> str: + # replace match with the middle character repeated 5 times + return self.group(0)[1].upper()*5 + +def test_pattern_patching(): + of = NamedTemporaryFile('wt') + of.writelines(['one line\n', + 'this pattern will be patched: defbbahij\n', + 'third line\n', + 'another pattern: jihaabfed']) + of.flush() + + files = FileList() + f = FileInfo(files, of.name) + f.load() + matches = f.matches_of_type(BasicPattern) + assert len(matches) == 2 + p2 = matches[1] + + # manually add patch, to see if .append() works: + f.patches.append(p2.append('XXX')) + + # apply all patches: + f.gen_patches(matches) + patched = f.get_patched_content() + assert patched == ('one line\n'+ + 'this pattern will be patched: defBBBBBhij\n'+ + 'third line\n'+ + 'another pattern: jihAAAAAXXXfed') + +class Function(FileMatch): + regexp = S(r'BEGIN\s+', NAMED('name', RE_IDENTIFIER), r'\n', + r'(.*\n)*?END\n') + +class Statement(FileMatch): + regexp = S(r'^\s*', NAMED('name', RE_IDENTIFIER), r'\(\)\n') + +def test_container_match(): + of = NamedTemporaryFile('wt') + of.writelines(['statement1()\n', + 'statement2()\n', + 'BEGIN function1\n', + ' statement3()\n', + ' statement4()\n', + 'END\n', + 'BEGIN function2\n', + ' statement5()\n', + ' statement6()\n', + 'END\n', + 'statement7()\n']) + of.flush() + + files = FileList() + f = FileInfo(files, of.name) + f.load() + assert len(f.matches_of_type(Function)) == 2 + print(' '.join(m.name for m in f.matches_of_type(Statement))) + assert len(f.matches_of_type(Statement)) == 7 + + f1 = f.find_match(Function, 'function1') + f2 = f.find_match(Function, 'function2') + st1 = f.find_match(Statement, 'statement1') + st2 = f.find_match(Statement, 'statement2') + st3 = f.find_match(Statement, 'statement3') + st4 = f.find_match(Statement, 'statement4') + st5 = f.find_match(Statement, 'statement5') + st6 = f.find_match(Statement, 'statement6') + st7 = f.find_match(Statement, 'statement7') + + assert not f1.contains(st1) + assert not f1.contains(st2) + assert not f1.contains(st2) + assert f1.contains(st3) + assert f1.contains(st4) + assert not f1.contains(st5) + assert not f1.contains(st6) + assert not f1.contains(st7) + + assert not f2.contains(st1) + assert not f2.contains(st2) + assert not f2.contains(st2) + assert not f2.contains(st3) + assert not f2.contains(st4) + assert f2.contains(st5) + assert f2.contains(st6) + assert not f2.contains(st7) diff --git a/scripts/codeconverter/codeconverter/test_regexps.py b/scripts/codeconverter/codeconverter/test_regexps.py new file mode 100644 index 000000000..a445634d8 --- /dev/null +++ b/scripts/codeconverter/codeconverter/test_regexps.py @@ -0,0 +1,282 @@ +# Copyright (C) 2020 Red Hat Inc. +# +# Authors: +# Eduardo Habkost <ehabkost@redhat.com> +# +# This work is licensed under the terms of the GNU GPL, version 2. See +# the COPYING file in the top-level directory. +from .regexps import * +from .qom_macros import * +from .qom_type_info import * + +def test_res() -> None: + def fullmatch(regexp, s): + return re.fullmatch(regexp, s, re.MULTILINE) + + assert fullmatch(RE_IDENTIFIER, 'sizeof') + assert fullmatch(RE_IDENTIFIER, 'X86CPU') + assert fullmatch(RE_FUN_CALL, 'sizeof(X86CPU)') + assert fullmatch(RE_IDENTIFIER, 'X86_CPU_TYPE_NAME') + assert fullmatch(RE_SIMPLE_VALUE, '"base"') + print(RE_FUN_CALL) + assert fullmatch(RE_FUN_CALL, 'X86_CPU_TYPE_NAME("base")') + print(RE_TI_FIELD_INIT) + assert fullmatch(RE_TI_FIELD_INIT, '.name = X86_CPU_TYPE_NAME("base"),\n') + + + assert fullmatch(RE_MACRO_CONCAT, 'TYPE_ASPEED_GPIO "-ast2600"') + assert fullmatch(RE_EXPRESSION, 'TYPE_ASPEED_GPIO "-ast2600"') + + print(RE_MACRO_DEFINE) + assert re.search(RE_MACRO_DEFINE, r''' + #define OFFSET_CHECK(c) \ + do { \ + if (!(c)) { \ + goto bad_offset; \ + } \ + } while (0) + ''', re.MULTILINE) + + print(RE_CHECK_MACRO) + print(CPP_SPACE) + assert not re.match(RE_CHECK_MACRO, r''' + #define OFFSET_CHECK(c) \ + do { \ + if (!(c)) { \ + goto bad_offset; \ + } \ + } while (0)''', re.MULTILINE) + + print(RE_CHECK_MACRO) + assert fullmatch(RE_CHECK_MACRO, r'''#define PCI_DEVICE(obj) \ + OBJECT_CHECK(PCIDevice, (obj), TYPE_PCI_DEVICE) +''') + assert fullmatch(RE_CHECK_MACRO, r'''#define COLLIE_MACHINE(obj) \ + OBJECT_CHECK(CollieMachineState, obj, TYPE_COLLIE_MACHINE) +''') + + print(RE_TYPEINFO_START) + assert re.search(RE_TYPEINFO_START, r''' + cc->open = qmp_chardev_open_file; +} + +static const TypeInfo char_file_type_info = { + .name = TYPE_CHARDEV_FILE, +#ifdef _WIN32 + .parent = TYPE_CHARDEV_WIN, +''', re.MULTILINE) + assert re.search(RE_TYPEINFO_START, r''' + TypeInfo ti = { + .name = armsse_variants[i].name, + .parent = TYPE_ARMSSE, + .class_init = armsse_class_init, + .class_data = (void *)&armsse_variants[i], + };''', re.MULTILINE) + + print(RE_ARRAY_ITEM) + assert fullmatch(RE_ARRAY_ITEM, '{ TYPE_HOTPLUG_HANDLER },') + assert fullmatch(RE_ARRAY_ITEM, '{ TYPE_ACPI_DEVICE_IF },') + assert fullmatch(RE_ARRAY_ITEM, '{ }') + assert fullmatch(RE_ARRAY_CAST, '(InterfaceInfo[])') + assert fullmatch(RE_ARRAY, '''(InterfaceInfo[]) { + { TYPE_HOTPLUG_HANDLER }, + { TYPE_ACPI_DEVICE_IF }, + { } + }''') + print(RE_COMMENT) + assert fullmatch(RE_COMMENT, r'''/* multi-line + * comment + */''') + + print(RE_TI_FIELDS) + assert fullmatch(RE_TI_FIELDS, + r'''/* could be TYPE_SYS_BUS_DEVICE (or LPC etc) */ + .parent = TYPE_DEVICE, +''') + assert fullmatch(RE_TI_FIELDS, r'''.name = TYPE_TPM_CRB, + /* could be TYPE_SYS_BUS_DEVICE (or LPC etc) */ + .parent = TYPE_DEVICE, + .instance_size = sizeof(CRBState), + .class_init = tpm_crb_class_init, + .interfaces = (InterfaceInfo[]) { + { TYPE_TPM_IF }, + { } + } +''') + assert fullmatch(RE_TI_FIELDS + SP + RE_COMMENTS, + r'''.name = TYPE_PALM_MISC_GPIO, + .parent = TYPE_SYS_BUS_DEVICE, + .instance_size = sizeof(PalmMiscGPIOState), + .instance_init = palm_misc_gpio_init, + /* + * No class init required: device has no internal state so does not + * need to set up reset or vmstate, and has no realize method. + */''') + + print(TypeInfoVar.regexp) + test_empty = 'static const TypeInfo x86_base_cpu_type_info = {\n'+\ + '};\n'; + assert fullmatch(TypeInfoVar.regexp, test_empty) + + test_simple = r''' + static const TypeInfo x86_base_cpu_type_info = { + .name = X86_CPU_TYPE_NAME("base"), + .parent = TYPE_X86_CPU, + .class_init = x86_cpu_base_class_init, + }; + ''' + assert re.search(TypeInfoVar.regexp, test_simple, re.MULTILINE) + + test_interfaces = r''' + static const TypeInfo acpi_ged_info = { + .name = TYPE_ACPI_GED, + .parent = TYPE_SYS_BUS_DEVICE, + .instance_size = sizeof(AcpiGedState), + .instance_init = acpi_ged_initfn, + .class_init = acpi_ged_class_init, + .interfaces = (InterfaceInfo[]) { + { TYPE_HOTPLUG_HANDLER }, + { TYPE_ACPI_DEVICE_IF }, + { } + } + }; + ''' + assert re.search(TypeInfoVar.regexp, test_interfaces, re.MULTILINE) + + test_comments = r''' + static const TypeInfo palm_misc_gpio_info = { + .name = TYPE_PALM_MISC_GPIO, + .parent = TYPE_SYS_BUS_DEVICE, + .instance_size = sizeof(PalmMiscGPIOState), + .instance_init = palm_misc_gpio_init, + /* + * No class init required: device has no internal state so does not + * need to set up reset or vmstate, and has no realize method. + */ + }; + ''' + assert re.search(TypeInfoVar.regexp, test_comments, re.MULTILINE) + + test_comments = r''' + static const TypeInfo tpm_crb_info = { + .name = TYPE_TPM_CRB, + /* could be TYPE_SYS_BUS_DEVICE (or LPC etc) */ + .parent = TYPE_DEVICE, + .instance_size = sizeof(CRBState), + .class_init = tpm_crb_class_init, + .interfaces = (InterfaceInfo[]) { + { TYPE_TPM_IF }, + { } + } + }; + ''' + assert re.search(TypeInfoVar.regexp, test_comments, re.MULTILINE) + +def test_struct_re(): + print('---') + print(RE_STRUCT_TYPEDEF) + assert re.search(RE_STRUCT_TYPEDEF, r''' +typedef struct TCGState { + AccelState parent_obj; + + bool mttcg_enabled; + unsigned long tb_size; +} TCGState; +''', re.MULTILINE) + + assert re.search(RE_STRUCT_TYPEDEF, r''' +typedef struct { + ISADevice parent_obj; + + QEMUSoundCard card; + uint32_t freq; + uint32_t port; + int ticking[2]; + int enabled; + int active; + int bufpos; +#ifdef DEBUG + int64_t exp[2]; +#endif + int16_t *mixbuf; + uint64_t dexp[2]; + SWVoiceOut *voice; + int left, pos, samples; + QEMUAudioTimeStamp ats; + FM_OPL *opl; + PortioList port_list; +} AdlibState; +''', re.MULTILINE) + + false_positive = r''' +typedef struct dma_pagetable_entry { + int32_t frame; + int32_t owner; +} A B C D E; +struct foo { + int x; +} some_variable; +''' + assert not re.search(RE_STRUCT_TYPEDEF, false_positive, re.MULTILINE) + +def test_initial_includes(): + print(InitialIncludes.regexp) + c = ''' +#ifndef HW_FLASH_H +#define HW_FLASH_H + +/* NOR flash devices */ + +#include "qom/object.h" +#include "exec/hwaddr.h" + +/* pflash_cfi01.c */ +''' + print(repr(list(m.groupdict() for m in InitialIncludes.finditer(c)))) + m = InitialIncludes.domatch(c) + assert m + print(repr(m.group(0))) + assert m.group(0).endswith('#include "exec/hwaddr.h"\n') + + c = '''#ifndef QEMU_VIRTIO_9P_H +#define QEMU_VIRTIO_9P_H + +#include "standard-headers/linux/virtio_9p.h" +#include "hw/virtio/virtio.h" +#include "9p.h" + + +''' + print(repr(list(m.groupdict() for m in InitialIncludes.finditer(c)))) + m = InitialIncludes.domatch(c) + assert m + print(repr(m.group(0))) + assert m.group(0).endswith('#include "9p.h"\n') + + c = '''#include "qom/object.h" +/* + * QEMU ES1370 emulation +... + */ + +/* #define DEBUG_ES1370 */ +/* #define VERBOSE_ES1370 */ +#define SILENT_ES1370 + +#include "qemu/osdep.h" +#include "hw/audio/soundhw.h" +#include "audio/audio.h" +#include "hw/pci/pci.h" +#include "migration/vmstate.h" +#include "qemu/module.h" +#include "sysemu/dma.h" + +/* Missing stuff: + SCTRL_P[12](END|ST)INC +''' + print(repr(list(m.groupdict() for m in InitialIncludes.finditer(c)))) + m = InitialIncludes.domatch(c) + assert m + print(repr(m.group(0))) + assert m.group(0).endswith('#include "sysemu/dma.h"\n') + diff --git a/scripts/codeconverter/codeconverter/utils.py b/scripts/codeconverter/codeconverter/utils.py new file mode 100644 index 000000000..760ab7eec --- /dev/null +++ b/scripts/codeconverter/codeconverter/utils.py @@ -0,0 +1,72 @@ +# Copyright (C) 2020 Red Hat Inc. +# +# Authors: +# Eduardo Habkost <ehabkost@redhat.com> +# +# This work is licensed under the terms of the GNU GPL, version 2. See +# the COPYING file in the top-level directory. +from typing import * + +import logging +logger = logging.getLogger(__name__) +DBG = logger.debug +INFO = logger.info +WARN = logger.warning + +T = TypeVar('T') +def opt_compare(a: T, b: T) -> bool: + """Compare two values, ignoring mismatches if one of them is None""" + return (a is None) or (b is None) or (a == b) + +def merge(a: T, b: T) -> T: + """Merge two values if they matched using opt_compare()""" + assert opt_compare(a, b) + if a is None: + return b + else: + return a + +def test_comp_merge(): + assert opt_compare(None, 1) == True + assert opt_compare(2, None) == True + assert opt_compare(1, 1) == True + assert opt_compare(1, 2) == False + + assert merge(None, None) is None + assert merge(None, 10) == 10 + assert merge(10, None) == 10 + assert merge(10, 10) == 10 + + +LineNumber = NewType('LineNumber', int) +ColumnNumber = NewType('ColumnNumber', int) +class LineAndColumn(NamedTuple): + line: int + col: int + + def __str__(self): + return '%d:%d' % (self.line, self.col) + +def line_col(s, position: int) -> LineAndColumn: + """Return line and column for a char position in string + + Character position starts in 0, but lines and columns start in 1. + """ + before = s[:position] + lines = before.split('\n') + line = len(lines) + col = len(lines[-1]) + 1 + return LineAndColumn(line, col) + +def test_line_col(): + assert line_col('abc\ndefg\nhijkl', 0) == (1, 1) + assert line_col('abc\ndefg\nhijkl', 2) == (1, 3) + assert line_col('abc\ndefg\nhijkl', 3) == (1, 4) + assert line_col('abc\ndefg\nhijkl', 4) == (2, 1) + assert line_col('abc\ndefg\nhijkl', 10) == (3, 2) + +def not_optional(arg: Optional[T]) -> T: + assert arg is not None + return arg + +__all__ = ['not_optional', 'opt_compare', 'merge', 'line_col', 'LineAndColumn']
\ No newline at end of file diff --git a/scripts/codeconverter/converter.py b/scripts/codeconverter/converter.py new file mode 100755 index 000000000..75cb515d9 --- /dev/null +++ b/scripts/codeconverter/converter.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# QEMU library +# +# Copyright (C) 2020 Red Hat Inc. +# +# Authors: +# Eduardo Habkost <ehabkost@redhat.com> +# +# This work is licensed under the terms of the GNU GPL, version 2. See +# the COPYING file in the top-level directory. +# +import sys +import argparse +import os +import os.path +import re +from typing import * + +from codeconverter.patching import FileInfo, match_class_dict, FileList +import codeconverter.qom_macros +from codeconverter.qom_type_info import TI_FIELDS, type_infos, TypeInfoVar + +import logging +logger = logging.getLogger(__name__) +DBG = logger.debug +INFO = logger.info +WARN = logger.warning + +def process_all_files(parser: argparse.ArgumentParser, args: argparse.Namespace) -> None: + DBG("filenames: %r", args.filenames) + + files = FileList() + files.extend(FileInfo(files, fn, args.force) for fn in args.filenames) + for f in files: + DBG('opening %s', f.filename) + f.load() + + if args.table: + fields = ['filename', 'variable_name'] + TI_FIELDS + print('\t'.join(fields)) + for f in files: + for t in f.matches_of_type(TypeInfoVar): + assert isinstance(t, TypeInfoVar) + values = [f.filename, t.name] + \ + [t.get_raw_initializer_value(f) + for f in TI_FIELDS] + DBG('values: %r', values) + assert all('\t' not in v for v in values) + values = [v.replace('\n', ' ').replace('"', '') for v in values] + print('\t'.join(values)) + return + + match_classes = match_class_dict() + if not args.patterns: + parser.error("--pattern is required") + + classes = [p for arg in args.patterns + for p in re.split(r'[\s,]', arg) + if p.strip()] + for c in classes: + if c not in match_classes \ + or not match_classes[c].regexp: + print("Invalid pattern name: %s" % (c), file=sys.stderr) + print("Valid patterns:", file=sys.stderr) + print(PATTERN_HELP, file=sys.stderr) + sys.exit(1) + + DBG("classes: %r", classes) + files.patch_content(max_passes=args.passes, class_names=classes) + + for f in files: + #alltypes.extend(f.type_infos) + #full_types.extend(f.full_types()) + + if not args.dry_run: + if args.inplace: + f.patch_inplace() + if args.diff: + f.show_diff() + if not args.diff and not args.inplace: + f.write_to_file(sys.stdout) + sys.stdout.flush() + + +PATTERN_HELP = ('\n'.join(" %s: %s" % (n, str(c.__doc__).strip()) + for (n,c) in sorted(match_class_dict().items()) + if c.has_replacement_rule())) + +def main() -> None: + p = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument('filenames', nargs='+') + p.add_argument('--passes', type=int, default=1, + help="Number of passes (0 means unlimited)") + p.add_argument('--pattern', required=True, action='append', + default=[], dest='patterns', + help="Pattern to scan for") + p.add_argument('--inplace', '-i', action='store_true', + help="Patch file in place") + p.add_argument('--dry-run', action='store_true', + help="Don't patch files or print patching results") + p.add_argument('--force', '-f', action='store_true', + help="Perform changes even if not completely safe") + p.add_argument('--diff', action='store_true', + help="Print diff output on stdout") + p.add_argument('--debug', '-d', action='store_true', + help="Enable debugging") + p.add_argument('--verbose', '-v', action='store_true', + help="Verbose logging on stderr") + p.add_argument('--table', action='store_true', + help="Print CSV table of type information") + p.add_argument_group("Valid pattern names", + PATTERN_HELP) + args = p.parse_args() + + loglevel = (logging.DEBUG if args.debug + else logging.INFO if args.verbose + else logging.WARN) + logging.basicConfig(format='%(levelname)s: %(message)s', level=loglevel) + DBG("args: %r", args) + process_all_files(p, args) + +if __name__ == '__main__': + main()
\ No newline at end of file |