#!/usr/bin/env python3 # pylint: disable=C0114,C0115,C0116,C0209,C0302,R0902,R0911,R0912,R0914,R0915,E1101 # # Copyright 2022-2023 by Wilson Snyder. Verilator is free software; you # can redistribute it and/or modify it under the terms of either the GNU Lesser # General Public License Version 3 or the Apache License 2.0. # SPDX-License-Identifier: LGPL-3.0-only OR Apache-2.0 import argparse import os import sys import shlex from typing import Callable, Iterable, Optional, Union import dataclasses from dataclasses import dataclass import enum from enum import Enum import multiprocessing import tempfile import clang.cindex from clang.cindex import CursorKind, Index, TranslationUnitSaveError, TranslationUnitLoadError def fully_qualified_name(node): if node is None: return [] if node.kind == CursorKind.TRANSLATION_UNIT: return [] res = fully_qualified_name(node.semantic_parent) if res: return res + ([node.displayname] if node.displayname else []) return [node.displayname] if node.displayname else [] @dataclass class VlAnnotations: mt_start: bool = False mt_safe: bool = False stable_tree: bool = False mt_safe_postinit: bool = False mt_unsafe: bool = False mt_unsafe_one: bool = False pure: bool = False guarded: bool = False requires: bool = False excludes: bool = False acquire: bool = False release: bool = False def is_mt_safe_context(self): return (not (self.mt_unsafe or self.mt_unsafe_one) and (self.mt_safe or self.mt_start)) def is_pure_context(self): return self.pure def is_stabe_tree_context(self): return self.stable_tree def is_mt_unsafe_call(self): return self.mt_unsafe or self.mt_unsafe_one def is_mt_safe_call(self): return (not self.is_mt_unsafe_call() and (self.mt_safe or self.mt_safe_postinit or self.pure or self.requires or self.excludes or self.acquire or self.release)) def is_pure_call(self): return self.pure def is_stabe_tree_call(self): return self.stable_tree def __or__(self, other: "VlAnnotations"): result = VlAnnotations() for key, value in dataclasses.asdict(self).items(): setattr(result, key, value | getattr(other, key)) return result def is_empty(self): for value in dataclasses.asdict(self).values(): if value: return False return True def __str__(self): result = [] for field, value in dataclasses.asdict(self).items(): if value: result.append(field) return ", ".join(result) @staticmethod def from_nodes_list(nodes: Iterable): result = VlAnnotations() for node in nodes: if node.kind == CursorKind.ANNOTATE_ATTR: if node.displayname == "MT_START": result.mt_start = True elif node.displayname == "MT_SAFE": result.mt_safe = True elif node.displayname == "MT_STABLE": result.stable_tree = True elif node.displayname == "MT_SAFE_POSTINIT": result.mt_safe_postinit = True elif node.displayname == "MT_UNSAFE": result.mt_unsafe = True elif node.displayname == "MT_UNSAFE_ONE": result.mt_unsafe_one = True elif node.displayname == "PURE": result.pure = True elif node.displayname in ["ACQUIRE", "ACQUIRE_SHARED"]: result.acquire = True elif node.displayname in ["RELEASE", "RELEASE_SHARED"]: result.release = True elif node.displayname == "REQUIRES": result.requires = True elif node.displayname in ["EXCLUDES", "MT_SAFE_EXCLUDES"]: result.excludes = True elif node.displayname == "GUARDED_BY": result.guarded = True # Attributes are always at the beginning elif not node.kind.is_attribute(): break return result class FunctionType(Enum): UNKNOWN = enum.auto() FUNCTION = enum.auto() METHOD = enum.auto() STATIC_METHOD = enum.auto() CONSTRUCTOR = enum.auto() @staticmethod def from_node(node: clang.cindex.Cursor): if node is None: return FunctionType.UNKNOWN if node.kind == CursorKind.FUNCTION_DECL: return FunctionType.FUNCTION if node.kind == CursorKind.CXX_METHOD and node.is_static_method(): return FunctionType.STATIC_METHOD if node.kind == CursorKind.CXX_METHOD: return FunctionType.METHOD if node.kind == CursorKind.CONSTRUCTOR: return FunctionType.CONSTRUCTOR return FunctionType.UNKNOWN @dataclass(eq=False) class FunctionInfo: name_parts: list[str] usr: str file: str line: int annotations: VlAnnotations ftype: FunctionType _hash: Optional[int] = dataclasses.field(default=None, init=False, repr=False) @property def name(self): return "::".join(self.name_parts) def __str__(self): return f"[{self.name}@{self.file}:{self.line}]" def __hash__(self): if not self._hash: self._hash = hash(f"{self.usr}:{self.file}:{self.line}") return self._hash def __eq__(self, other): return (self.usr == other.usr and self.file == other.file and self.line == other.line) def copy(self, /, **changes): return dataclasses.replace(self, **changes) @staticmethod def from_node(node: clang.cindex.Cursor, refd: Optional[clang.cindex.Cursor] = None, annotations: Optional[VlAnnotations] = None): file = os.path.abspath(node.location.file.name) line = node.location.line if annotations is None: annotations = VlAnnotations.from_nodes_list(node.get_children()) if refd is None: refd = node.referenced if refd is not None: refd = refd.canonical assert refd is not None name_parts = fully_qualified_name(refd) usr = refd.get_usr() ftype = FunctionType.from_node(refd) return FunctionInfo(name_parts, usr, file, line, annotations, ftype) class DiagnosticKind(Enum): ANNOTATIONS_DEF_DECL_MISMATCH = enum.auto() NON_PURE_CALL_IN_PURE_CTX = enum.auto() NON_MT_SAFE_CALL_IN_MT_SAFE_CTX = enum.auto() NON_STABLE_TREE_CALL_IN_STABLE_TREE_CTX = enum.auto() def __lt__(self, other): return self.value < other.value @dataclass class Diagnostic: target: FunctionInfo source: FunctionInfo source_ctx: FunctionInfo kind: DiagnosticKind _hash: Optional[int] = dataclasses.field(default=None, init=False, repr=False) def __hash__(self): if not self._hash: self._hash = hash( hash(self.target) ^ hash(self.source_ctx) ^ hash(self.kind)) return self._hash class CallAnnotationsValidator: def __init__(self, diagnostic_cb: Callable[[Diagnostic], None], is_ignored_top_level: Callable[[clang.cindex.Cursor], bool], is_ignored_def: Callable[ [clang.cindex.Cursor, clang.cindex.Cursor], bool], is_ignored_call: Callable[[clang.cindex.Cursor], bool]): self._diagnostic_cb = diagnostic_cb self._is_ignored_top_level = is_ignored_top_level self._is_ignored_call = is_ignored_call self._is_ignored_def = is_ignored_def self._index = Index.create() self._processed_headers: set[str] = set() # Current context self._call_location: Optional[FunctionInfo] = None self._caller: Optional[FunctionInfo] = None self._level: int = 0 self._constructor_context: int = 0 def compile_and_analyze_file(self, source_file: str, compiler_args: list[str], build_dir: Optional[str]): filename = os.path.abspath(source_file) initial_cwd = "." if build_dir: initial_cwd = os.getcwd() os.chdir(build_dir) translation_unit = self._index.parse(filename, compiler_args) has_errors = False for diag in translation_unit.diagnostics: if diag.severity > clang.cindex.Diagnostic.Error: has_errors = True if translation_unit and not has_errors: self.process_translation_unit(translation_unit) else: print(f"%Error: parsing failed: {filename}", file=sys.stderr) if build_dir: os.chdir(initial_cwd) def emit_diagnostic(self, target: Union[FunctionInfo, clang.cindex.Cursor], kind: DiagnosticKind): assert self._caller is not None assert self._call_location is not None source = self._caller source_ctx = self._call_location if isinstance(target, FunctionInfo): self._diagnostic_cb(Diagnostic(target, source, source_ctx, kind)) else: self._diagnostic_cb( Diagnostic(FunctionInfo.from_node(target), source, source_ctx, kind)) def iterate_children(self, children: Iterable[clang.cindex.Cursor], handler: Callable[[clang.cindex.Cursor], None]): if children: self._level += 1 for child in children: handler(child) self._level -= 1 @staticmethod def get_referenced_node_info( node: clang.cindex.Cursor ) -> tuple[bool, Optional[clang.cindex.Cursor], VlAnnotations, Iterable[clang.cindex.Cursor]]: if not node.spelling and not node.displayname: return (False, None, VlAnnotations(), []) refd = node.referenced if refd is None: raise ValueError("The node does not specify referenced node.") refd = refd.canonical children = list(refd.get_children()) annotations = VlAnnotations.from_nodes_list(children) return (True, refd, annotations, children) def check_mt_safe_call(self, node: clang.cindex.Cursor, refd: clang.cindex.Cursor, annotations: VlAnnotations): is_mt_safe = False if annotations.is_mt_safe_call(): is_mt_safe = True elif not annotations.is_mt_unsafe_call(): # Check whether the object the method is called on is mt-safe def find_object_ref(node): try: node = next(node.get_children()) if node.kind == CursorKind.DECL_REF_EXPR: # Operator on an argument or local object return node if node.kind != CursorKind.MEMBER_REF_EXPR: return None if node.referenced and node.referenced.kind == CursorKind.FIELD_DECL: # Operator on a member object return node node = next(node.get_children()) if node.kind == CursorKind.UNEXPOSED_EXPR: node = next(node.get_children()) return node except StopIteration: return None refn = find_object_ref(node) if self._constructor_context and not refn: # we are in constructor and no object reference means # we are calling local method. It is MT safe # only if this method is also only calling local methods or # MT-safe methods self.iterate_children(refd.get_children(), self.dispatch_node_inside_definition) is_mt_safe = True # class/struct member elif refn and refn.kind == CursorKind.MEMBER_REF_EXPR and refn.referenced: refn = refn.referenced refna = VlAnnotations.from_nodes_list(refn.get_children()) if refna.guarded or self._constructor_context: is_mt_safe = True # variable elif refn and refn.kind == CursorKind.DECL_REF_EXPR and refn.referenced: # This is probably a local or an argument. # Calling methods on local pointers or references is MT-safe, # but on argument pointers or references is not. if "*" not in refn.type.spelling and "&" not in refn.type.spelling: is_mt_safe = True # local variable if refn.referenced.kind == CursorKind.VAR_DECL: is_mt_safe = True elif refn and refn.kind == CursorKind.CALL_EXPR: if self._constructor_context: # call to local function from constructor context # safe if this function also calling local methods or # MT-safe methods self.dispatch_call_node(refn) is_mt_safe = True return is_mt_safe # Call handling def process_method_call(self, node: clang.cindex.Cursor, refd: clang.cindex.Cursor, annotations: VlAnnotations): assert self._call_location ctx = self._call_location.annotations # MT-safe context if ctx.is_mt_safe_context(): if not self.check_mt_safe_call(node, refd, annotations): self.emit_diagnostic( FunctionInfo.from_node(refd, refd, annotations), DiagnosticKind.NON_MT_SAFE_CALL_IN_MT_SAFE_CTX) # stable tree context if ctx.is_stabe_tree_context(): if not (annotations.is_stabe_tree_call() or annotations.is_pure_call() or self.check_mt_safe_call(node, refd, annotations)): self.emit_diagnostic( FunctionInfo.from_node(refd, refd, annotations), DiagnosticKind.NON_STABLE_TREE_CALL_IN_STABLE_TREE_CTX) # pure context if ctx.is_pure_context(): if not annotations.is_pure_call(): self.emit_diagnostic( FunctionInfo.from_node(refd, refd, annotations), DiagnosticKind.NON_PURE_CALL_IN_PURE_CTX) def process_function_call(self, refd: clang.cindex.Cursor, annotations: VlAnnotations): assert self._call_location ctx = self._call_location.annotations # MT-safe context if ctx.is_mt_safe_context(): if not annotations.is_mt_safe_call(): self.emit_diagnostic( FunctionInfo.from_node(refd, refd, annotations), DiagnosticKind.NON_MT_SAFE_CALL_IN_MT_SAFE_CTX) # stable tree context if ctx.is_stabe_tree_context(): if not (annotations.is_pure_call() or annotations.is_mt_safe_call() or annotations.is_stabe_tree_call()): self.emit_diagnostic( FunctionInfo.from_node(refd, refd, annotations), DiagnosticKind.NON_STABLE_TREE_CALL_IN_STABLE_TREE_CTX) # pure context if ctx.is_pure_context(): if not annotations.is_pure_call(): self.emit_diagnostic( FunctionInfo.from_node(refd, refd, annotations), DiagnosticKind.NON_PURE_CALL_IN_PURE_CTX) def process_constructor_call(self, refd: clang.cindex.Cursor, annotations: VlAnnotations): assert self._call_location ctx = self._call_location.annotations # Constructors are OK in MT-safe context # only if they call local methods or MT-safe functions. if ctx.is_mt_safe_context() or self._constructor_context: self._constructor_context += 1 self.iterate_children(refd.get_children(), self.dispatch_node_inside_definition) self._constructor_context -= 1 # stable tree context if ctx.is_stabe_tree_context(): self._constructor_context += 1 self.iterate_children(refd.get_children(), self.dispatch_node_inside_definition) self._constructor_context -= 1 # pure context if ctx.is_pure_context(): if not annotations.is_pure_call( ) and not refd.is_default_constructor(): self.emit_diagnostic( FunctionInfo.from_node(refd, refd, annotations), DiagnosticKind.NON_PURE_CALL_IN_PURE_CTX) def dispatch_call_node(self, node: clang.cindex.Cursor): [supported, refd, annotations, _] = self.get_referenced_node_info(node) if not supported: self.iterate_children(node.get_children(), self.dispatch_node_inside_definition) return assert refd is not None if self._is_ignored_call(refd): return assert self._call_location is not None node_file = os.path.abspath(node.location.file.name) self._call_location = self._call_location.copy(file=node_file, line=node.location.line) # Standalone functions and static class methods if (refd.kind == CursorKind.FUNCTION_DECL or refd.kind == CursorKind.CXX_METHOD and refd.is_static_method()): self.process_function_call(refd, annotations) self.iterate_children(node.get_children(), self.dispatch_node_inside_definition) return # Function pointer if refd.kind in [ CursorKind.VAR_DECL, CursorKind.FIELD_DECL, CursorKind.PARM_DECL ]: self.process_function_call(refd, annotations) self.iterate_children(node.get_children(), self.dispatch_node_inside_definition) return # Non-static class methods if refd.kind == CursorKind.CXX_METHOD: self.process_method_call(node, refd, annotations) self.iterate_children(node.get_children(), self.dispatch_node_inside_definition) return # Conversion method (e.g. `operator int()`) if refd.kind == CursorKind.CONVERSION_FUNCTION: self.process_method_call(node, refd, annotations) self.iterate_children(node.get_children(), self.dispatch_node_inside_definition) return # Constructors if refd.kind == CursorKind.CONSTRUCTOR: self.process_constructor_call(refd, annotations) self.iterate_children(node.get_children(), self.dispatch_node_inside_definition) return # Ignore other callables print(f"{refd.location.file.name}:{refd.location.line}: " f"{refd.displayname} {refd.kind}\n" f" from: {node.location.file.name}:{node.location.line}") # Definition handling def dispatch_node_inside_definition(self, node: clang.cindex.Cursor): if node.kind == CursorKind.CALL_EXPR: self.dispatch_call_node(node) return None return self.iterate_children(node.get_children(), self.dispatch_node_inside_definition) def process_function_definition(self, node: clang.cindex.Cursor): [supported, refd, annotations, _] = self.get_referenced_node_info(node) if refd and self._is_ignored_def(node, refd): return None node_children = list(node.get_children()) if not supported: return self.iterate_children(node_children, self.dispatch_node) assert refd is not None def_annotations = VlAnnotations.from_nodes_list(node_children) if not (def_annotations.is_empty() or def_annotations == annotations): # Use definition's annotations for the diagnostic # source (i.e. the definition) self._caller = FunctionInfo.from_node(node, refd, def_annotations) self._call_location = self._caller self.emit_diagnostic( FunctionInfo.from_node(refd, refd, annotations), DiagnosticKind.ANNOTATIONS_DEF_DECL_MISMATCH) # Use concatenation of definition and declaration annotations # for callees validation. self._caller = FunctionInfo.from_node(node, refd, def_annotations | annotations) self._call_location = self._caller self.iterate_children(node_children, self.dispatch_node_inside_definition) self._call_location = None return None # Nodes not located inside definition def dispatch_node(self, node: clang.cindex.Cursor): if node.is_definition() and node.kind in [ CursorKind.CXX_METHOD, CursorKind.FUNCTION_DECL, CursorKind.CONSTRUCTOR, CursorKind.CONVERSION_FUNCTION ]: return self.process_function_definition(node) if node.is_definition() and node.kind in [ CursorKind.NAMESPACE, CursorKind.STRUCT_DECL, CursorKind.UNION_DECL, CursorKind.CLASS_DECL ]: return self.iterate_children(node.get_children(), self.dispatch_node) return self.iterate_children(node.get_children(), self.dispatch_node) def process_translation_unit( self, translation_unit: clang.cindex.TranslationUnit): self._level += 1 for child in translation_unit.cursor.get_children(): if self._is_ignored_top_level(child): continue if self._processed_headers: filename = os.path.abspath(child.location.file.name) if filename in self._processed_headers: continue self.dispatch_node(child) self._level -= 1 self._processed_headers.update([ os.path.abspath(str(hdr.source)) for hdr in translation_unit.get_includes() ]) @dataclass class CompileCommand: refid: int filename: str args: list[str] directory: str = dataclasses.field(default_factory=os.getcwd) def get_filter_funcs(verilator_root: str): verilator_root = os.path.abspath(verilator_root) + "/" def is_ignored_top_level(node: clang.cindex.Cursor) -> bool: # Anything defined in a header outside Verilator root if not node.location.file: return True filename = os.path.abspath(node.location.file.name) return not filename.startswith(verilator_root) def is_ignored_def(node: clang.cindex.Cursor, refd: clang.cindex.Cursor) -> bool: # __* if str(refd.spelling).startswith("__"): return True # Anything defined in a header outside Verilator root if not node.location.file: return True filename = os.path.abspath(node.location.file.name) if not filename.startswith(verilator_root): return True return False def is_ignored_call(refd: clang.cindex.Cursor) -> bool: # __* if str(refd.spelling).startswith("__"): return True # std::* fqn = fully_qualified_name(refd) if fqn and fqn[0] == "std": return True # Anything declared in a header outside Verilator root if not refd.location.file: return True filename = os.path.abspath(refd.location.file.name) if not filename.startswith(verilator_root): return True return False return (is_ignored_top_level, is_ignored_def, is_ignored_call) def precompile_header(compile_command: CompileCommand, tmp_dir: str) -> str: try: initial_cwd = os.getcwd() os.chdir(compile_command.directory) index = Index.create() translation_unit = index.parse(compile_command.filename, compile_command.args) for diag in translation_unit.diagnostics: if diag.severity > clang.cindex.Diagnostic.Error: pch_file = None break else: pch_file = os.path.join( tmp_dir, f"{compile_command.refid:02}_{os.path.basename(compile_command.filename)}.pch" ) translation_unit.save(pch_file) os.chdir(initial_cwd) if pch_file: return pch_file except (TranslationUnitSaveError, TranslationUnitLoadError, OSError): pass print( f"%Warning: Precompiling failed, skipping: {compile_command.filename}") return "" # Compile and analyze inputs in a single process. def run_analysis(ccl: Iterable[CompileCommand], pccl: Iterable[CompileCommand], diagnostic_cb: Callable[[Diagnostic], None], verilator_root: str): (is_ignored_top_level, is_ignored_def, is_ignored_call) = get_filter_funcs(verilator_root) prefix = "verilator_clang_check_attributes_" with tempfile.TemporaryDirectory(prefix=prefix) as tmp_dir: extra_args = [] for pcc in pccl: pch_file = precompile_header(pcc, tmp_dir) if pch_file: extra_args += ["-include-pch", pch_file] cav = CallAnnotationsValidator(diagnostic_cb, is_ignored_top_level, is_ignored_def, is_ignored_call) for compile_command in ccl: cav.compile_and_analyze_file(compile_command.filename, compile_command.args + extra_args, compile_command.directory) class ParallelAnalysisProcess: cav: Optional[CallAnnotationsValidator] = None diags: set[Diagnostic] = dataclasses.field(default_factory=set) tmp_dir: str = "" @staticmethod def init_data(verilator_root: str, tmp_dir: str): (is_ignored_top_level, is_ignored_def, is_ignored_call) = get_filter_funcs(verilator_root) ParallelAnalysisProcess.cav = CallAnnotationsValidator( ParallelAnalysisProcess._diagnostic_handler, is_ignored_top_level, is_ignored_def, is_ignored_call) ParallelAnalysisProcess.tmp_dir = tmp_dir @staticmethod def _diagnostic_handler(diag: Diagnostic): ParallelAnalysisProcess.diags.add(diag) @staticmethod def analyze_cpp_file(compile_command: CompileCommand) -> set[Diagnostic]: ParallelAnalysisProcess.diags = set() assert ParallelAnalysisProcess.cav is not None ParallelAnalysisProcess.cav.compile_and_analyze_file( compile_command.filename, compile_command.args, compile_command.directory) return ParallelAnalysisProcess.diags @staticmethod def precompile_header(compile_command: CompileCommand) -> str: return precompile_header(compile_command, ParallelAnalysisProcess.tmp_dir) # Compile and analyze inputs in multiple processes. def run_parallel_analysis(ccl: Iterable[CompileCommand], pccl: Iterable[CompileCommand], diagnostic_cb: Callable[[Diagnostic], None], jobs_count: int, verilator_root: str): prefix = "verilator_clang_check_attributes_" with tempfile.TemporaryDirectory(prefix=prefix) as tmp_dir: with multiprocessing.Pool( processes=jobs_count, initializer=ParallelAnalysisProcess.init_data, initargs=[verilator_root, tmp_dir]) as pool: extra_args = [] for pch_file in pool.imap_unordered( ParallelAnalysisProcess.precompile_header, pccl): if pch_file: extra_args += ["-include-pch", pch_file] if extra_args: for compile_command in ccl: compile_command.args = compile_command.args + extra_args for diags in pool.imap_unordered( ParallelAnalysisProcess.analyze_cpp_file, ccl, 1): for diag in diags: diagnostic_cb(diag) class TopDownSummaryPrinter(): @dataclass class FunctionCallees: info: FunctionInfo calees: set[FunctionInfo] mismatch: Optional[FunctionInfo] = None reason: Optional[DiagnosticKind] = None def __init__(self): self._is_first_group = True self._funcs: dict[str, TopDownSummaryPrinter.FunctionCallees] = {} self._unsafe_in_safe: set[str] = set() def begin_group(self, label): if not self._is_first_group: print() print(f"%Error: {label}") self._is_first_group = False def handle_diagnostic(self, diag: Diagnostic): usr = diag.source.usr func = self._funcs.get(usr, None) if func is None: func = TopDownSummaryPrinter.FunctionCallees(diag.source, set()) self._funcs[usr] = func func.reason = diag.kind if diag.kind == DiagnosticKind.ANNOTATIONS_DEF_DECL_MISMATCH: func.mismatch = diag.target else: func.calees.add(diag.target) self._unsafe_in_safe.add(diag.target.usr) def print_summary(self, root_dir: str): row_groups: dict[str, list[list[str]]] = {} column_widths = [0, 0] for func in sorted(self._funcs.values(), key=lambda func: (func.info.file, func.info.line, func.info.usr)): func_info = func.info relfile = os.path.relpath(func_info.file, root_dir) row_group = [] name = f"\"{func_info.name}\" " if func.reason == DiagnosticKind.ANNOTATIONS_DEF_DECL_MISMATCH: name += "declaration does not match definition" elif func.reason == DiagnosticKind.NON_MT_SAFE_CALL_IN_MT_SAFE_CTX: name += "is mtsafe but calls non-mtsafe function(s)" elif func.reason == DiagnosticKind.NON_PURE_CALL_IN_PURE_CTX: name += "is pure but calls non-pure function(s)" elif func.reason == DiagnosticKind.NON_STABLE_TREE_CALL_IN_STABLE_TREE_CTX: name += "calls stable_tree function(s) but isn't annotated as stable_tree" else: name += "for unknown reason (please add description)" if func.mismatch: mrelfile = os.path.relpath(func.mismatch.file, root_dir) row_group.append([ f"{mrelfile}:{func.mismatch.line}:", f"[{func.mismatch.annotations}]", func.mismatch.name + " [declaration]" ]) row_group.append([ f"{relfile}:{func_info.line}:", f"[{func_info.annotations}]", func_info.name ]) for callee in sorted(func.calees, key=lambda func: (func.file, func.line, func.usr)): crelfile = os.path.relpath(callee.file, root_dir) row_group.append([ f"{crelfile}:{callee.line}:", f"[{callee.annotations}]", " " + callee.name ]) row_groups[name] = row_group for row in row_group: for row_id, value in enumerate(row[0:-1]): column_widths[row_id] = max(column_widths[row_id], len(value)) for label, rows in sorted(row_groups.items(), key=lambda kv: kv[0]): self.begin_group(label) for row in rows: print(f"{row[0]:<{column_widths[0]}} " f"{row[1]:<{column_widths[1]}} " f"{row[2]}") print( f"Number of functions reported unsafe: {len(self._unsafe_in_safe)}" ) def main(): default_verilator_root = os.path.abspath( os.path.join(os.path.dirname(__file__), "..")) parser = argparse.ArgumentParser( allow_abbrev=False, formatter_class=argparse.RawDescriptionHelpFormatter, description="""Check function annotations for correctness""", epilog= """Copyright 2022-2023 by Wilson Snyder. Verilator is free software; you can redistribute it and/or modify it under the terms of either the GNU Lesser General Public License Version 3 or the Apache License 2.0. SPDX-License-Identifier: LGPL-3.0-only OR Apache-2.0""") parser.add_argument("--verilator-root", type=str, default=default_verilator_root, help="Path to Verilator sources root directory.") parser.add_argument("--jobs", "-j", type=int, default=0, help="Number of parallel jobs to use.") parser.add_argument("--cxxflags", type=str, default=None, help="Flags passed to clang++.") parser.add_argument( "--compilation-root", type=str, default=os.getcwd(), help="Directory used as CWD when compiling source files.") parser.add_argument( "-c", "--precompile", action="append", help="Header file to be precompiled and cached at the start.") parser.add_argument("file", type=str, nargs="+", help="Source file to analyze.") cmdline = parser.parse_args() if cmdline.jobs == 0: cmdline.jobs = max(1, len(os.sched_getaffinity(0))) if not cmdline.compilation_root: cmdline.compilation_root = cmdline.verilator_root verilator_root = os.path.abspath(cmdline.verilator_root) compilation_root = os.path.abspath(cmdline.compilation_root) default_cxx_flags = [ f"-I{verilator_root}/src", f"-I{verilator_root}/include", f"-I{verilator_root}/src/obj_opt", "-fcoroutines-ts", ] if cmdline.cxxflags is not None: cxxflags = shlex.split(cmdline.cxxflags) else: cxxflags = default_cxx_flags precompile_commands_list = [] if cmdline.precompile: hdr_cxxflags = ['-xc++-header'] + cxxflags for refid, file in enumerate(cmdline.precompile): filename = os.path.abspath(file) compile_command = CompileCommand(refid, filename, hdr_cxxflags, compilation_root) precompile_commands_list.append(compile_command) compile_commands_list = [] for refid, file in enumerate(cmdline.file): filename = os.path.abspath(file) compile_command = CompileCommand(refid, filename, cxxflags, compilation_root) compile_commands_list.append(compile_command) summary_printer = TopDownSummaryPrinter() if cmdline.jobs == 1: run_analysis(compile_commands_list, precompile_commands_list, summary_printer.handle_diagnostic, verilator_root) else: run_parallel_analysis(compile_commands_list, precompile_commands_list, summary_printer.handle_diagnostic, cmdline.jobs, verilator_root) summary_printer.print_summary(verilator_root) if __name__ == '__main__': main()