diff --git a/installation_procedure.txt b/installation_procedure.txt index 01331dd2..076ebf20 100644 --- a/installation_procedure.txt +++ b/installation_procedure.txt @@ -1,9 +1,9 @@ Reminder: this is for linux setup 1. Install flexpret by following it's instructions. Use branch gametime-support. Can use instructions of master branch. - ***Remember to export PATH=$PATH:/Users/abdallaeltayeb/Desktop/Gamtime_project/flexpret/emulator*** USE ABS PATH + ***Remember to export PATH=$PATH:/Users/I747530/Desktop/Gametime-project/flexpret/emulator*** USE ABS PATH export PATH=$PATH:/opt/riscv/bin - + /Users/I747530/Desktop/Gametime-project/gametime/installation_procedure.txt 2. Pull the lastest Gametime. git submodule update --init --recursive diff --git a/src/analyze_project.py b/src/analyze_project.py deleted file mode 100644 index 62350c62..00000000 --- a/src/analyze_project.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python - -def main(): - """Main function invoked when this script is run.""" - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/src/analyzer.py b/src/analyzer.py index d507b118..4f271447 100644 --- a/src/analyzer.py +++ b/src/analyzer.py @@ -1,34 +1,4 @@ #!/usr/bin/env python -import os -import shutil -import time -from typing import List, Tuple, Optional - -import numpy as np -import networkx as nx - -import clang_helper -import nx_helper -import pulp_helper -import inliner -import unroller -from defaults import config, logger -from file_helper import remove_all_except -from gametime_error import GameTimeError -from nx_helper import Dag, write_dag_to_dot_file -from path import Path -from path_analyzer import PathAnalyzer -from project_configuration import ProjectConfiguration -from path_generator import PathGenerator - -from numpy import dot, exp, eye -from numpy.linalg import det, inv, slogdet - -from backend.flexpret_backend.flexpret_backend import FlexpretBackend -from backend.x86_backend.x86_backend import X86Backend -from backend.arm_backend.arm_backend import ArmBackend -from backend.backend import Backend -from smt_solver.extract_labels import find_labels """Defines a class that maintains information about the code being analyzed, such as the name of the file that contains the code being analyzed and @@ -41,82 +11,100 @@ for details on the GameTime license and authors. """ + +import bz2 +import os +import pickle +import random +import shutil +import time +from copy import deepcopy + +from numpy import dot, exp, eye, genfromtxt, savetxt +from numpy.linalg import det, inv, slogdet + +import cilHelper +import inliner +import loopHandler +import merger +import nxHelper +import phoenixHelper +import pulpHelper +import networkx as nx +from defaults import config, logger +from fileHelper import createDir, removeAllExcept, removeFile +from gametimeError import GameTimeError +from nxHelper import Dag +from path import Path +from pathGenerator import PathGenerator +from smt.query import readQueryFromFile, Satisfiability + + class Analyzer(object): - """ - Maintains information about the code being analyzed, such as + """Maintains information about the code being analyzed, such as the name of the file that contains the code being analyzed and the basis paths of the code. - Parameters: - project_config: - Object that represents the configuration of a GameTime project. + Attributes: + projectConfig: + :class:`~gametime.projectConfiguration.ProjectConfiguration` + object that represents the configuration of a GameTime project. """ - - def __init__(self, project_config: ProjectConfiguration): + def __init__(self, projectConfig): ### CONFIGURATIONS ### #: :class:`~gametime.projectConfiguration.ProjectConfiguration` object #: that represents the configuration of a GameTime project. - self.project_config: ProjectConfiguration = project_config + self.projectConfig = projectConfig ### GRAPH INFORMATION ### #: Data structure for the DAG of the code being analyzed. - self.dag: Dag = Dag() + self.dag = Dag() ### PATHS INFORMATION ### #: Dimension of the vector representing each path. - self.path_dimension: int = 0 + self.pathDimension = 0 #: Basis matrix. - self.basis_matrix: Optional[np.ndarray] = None + self.basisMatrix = None #: Set whose elements are lists of edges that must not be taken #: together along any path through the DAG. For example, the element #: [e1, e2] means "if you take e1, you cannot take e2" and #: "if you take e2, you cannot take e1". - self.path_exclusive_constraints: List[List[Tuple[str, str]]] = [] + self.pathExclusiveConstraints = [] #: List whose elements are lists of edges that must be taken together, #: if at least one is taken along a path through the DAG. For example, #: the element [e1, e2] means "if you take e1, then you take e2". - self.path_bundled_constraints: List[List[Tuple[str, str]]] = [] + self.pathBundledConstraints = [] # Number of `bad' rows in the basis matrix. - self.num_bad_rows: int = 0 + self.numBadRows = 0 - # List of the Path objects associated with all_temp_files basis paths + # List of the Path objects associated with all basis paths # generated so far. - self.basis_paths: List[Path] = [] + self.basisPaths = [] # List of lists, each of which is a list of IDs of the nodes in # the DAG along each basis path. Each ID is a string. The lists are # arranged in the same order as the Path objects associated with - # the basis paths are arranged in the `basis_paths' list. + # the basis paths are arranged in the `basisPaths' list. # This list is maintained for efficiency purposes. - self.basis_paths_nodes: List[Path] = [] + self.basisPathsNodes = [] - # Specify default parameters for the values used with - # --ob_extraction flag. The values are outputted only - # when the flag is used. + # Specify default parameters for the values used with + # --ob_extraction flag. The values are outputted only + # when the flag is used. # Value of mu_max computed for the observed measurements - self.inferred_mu_max: int = 0 + self.inferredMuMax = 0 # The in predictions is error is 2 * inferredMuMax * errorScaleFactor - self.error_scale_factor: int = 0 - - self.dag_path: str = "" - - backend_dict = {"Flexpret": FlexpretBackend, "X86": X86Backend, "ARM": ArmBackend} - - if self.project_config.backend not in backend_dict: - raise GameTimeError("No valid backend specified") - - self.backend: Backend = backend_dict[self.project_config.backend](self.project_config) + self.errorScaleFactor = 0 # Finally, preprocess the file before analysis. self._preprocess() def _preprocess(self): - """ - Preprocesses the file before analysis. The preprocessing steps are: + """Preprocesses the file before analysis. The preprocessing steps are: 1. Create a temporary directory that will contain the files generated during analysis. 2. Copy the source file being analyzed into this temporary directory. @@ -124,787 +112,1309 @@ def _preprocess(self): unrolling and function inlining. """ # Check if the file to be analyzed exists. - orig_file = self.project_config.location_orig_file - project_temp_dir = self.project_config.location_temp_dir - if not os.path.exists(orig_file): - shutil.rmtree(project_temp_dir) - err_msg = "File to analyze not found: %s" % orig_file - raise GameTimeError(err_msg) - - # Check if the additional files to be analyzed exists. - additional_files = self.project_config.location_additional_files - if additional_files: - for additional_file in additional_files: - project_temp_dir = self.project_config.location_temp_dir - if not os.path.exists(additional_file): - shutil.rmtree(project_temp_dir) - err_msg = "External File to analyze not found: %s" % additional_file - raise GameTimeError(err_msg) + origFile = self.projectConfig.locationOrigFile + projectTempDir = self.projectConfig.locationTempDir + if not os.path.exists(origFile): + shutil.rmtree(projectTempDir) + errMsg = "File to analyze not found: %s" % origFile + raise GameTimeError(errMsg) # Remove any temporary directory created during a previous run # of the same GameTime project, and create a fresh new # temporary directory. - if os.path.exists(project_temp_dir): - if self.project_config.UNROLL_LOOPS: + if os.path.exists(projectTempDir): + if self.projectConfig.UNROLL_LOOPS: # If a previous run of the same GameTime project produced # a loop configuration file, and the current run involves # unrolling the loops that are configured in the file, # do not remove the file. - remove_all_except([config.TEMP_LOOP_CONFIG], project_temp_dir) + removeAllExcept([config.TEMP_LOOP_CONFIG], projectTempDir) else: - remove_all_except([], project_temp_dir) + removeAllExcept([], projectTempDir) else: - os.mkdir(project_temp_dir) - - os.chmod(project_temp_dir, 0o777) # make dir read and write by everyone + os.mkdir(projectTempDir) # Make a temporary copy of the original file to preprocess. - preprocessed_file = self.project_config.location_temp_file - shutil.copyfile(orig_file, preprocessed_file) + preprocessedFile = self.projectConfig.locationTempFile + shutil.copyfile(origFile, preprocessedFile) - processing: str = "" + # Preprocessing pass: merge other source files. + if len(self.projectConfig.merged) > 0: + self._runMerger() + + # Preprocessing pass: unroll loops. + if self.projectConfig.UNROLL_LOOPS: + self._runLoopUnroller() - processing = clang_helper.compile_to_llvm_for_analysis(self.project_config.location_orig_file, self.project_config.location_temp_dir, - f"{self.project_config.name_orig_no_extension}gt", self.project_config.included, self.project_config.compile_flags) - additional_files_processing = [] - if additional_files: - additional_files_processing = clang_helper.compile_list_to_llvm_for_analysis(self.project_config.location_additional_files, self.project_config.location_temp_dir, - self.project_config.included, self.project_config.compile_flags) - # Preprocessing pass: inline functions. - if self.project_config.inlined: # Note: This is made into a bool rather than a list - processing = self._run_inliner(input_file=processing, additional_files=additional_files_processing) + if len(self.projectConfig.inlined) > 0: + self._runInliner() + + # Preprocessing pass: run the file through CIL once more, + # to reduce the C file to the subset of constructs used by CIL + # for ease of analysis. + self._runCil() - # Preprocessing pass: unroll loops. - if self.project_config.UNROLL_LOOPS: - processing = self._run_loop_unroller(compiled_file=processing) - self.dag_path: str = clang_helper.generate_dot_file(processing, self.project_config.location_temp_dir) - self.preprocessed_path: str = processing # We are done with the preprocessing. logger.info("Preprocessing complete.") logger.info("") - def _run_loop_unroller(self, compiled_file: str) -> str: + ### PREPROCESSING HELPER FUNCTIONS ### + def _runMerger(self): + """As part of preprocessing, runs CIL on the source file under + analysis to merge other source files. A copy of the file that + results from the CIL preprocessing is made and renamed for use by + other preprocessing phases, and the file itself is renamed and + stored for later perusal. """ - As part of preprocessing, runs CIL on the source file under + preprocessedFile = self.projectConfig.locationTempFile + # Infer the name of the file that results from the CIL preprocessing. + cilFile = "%s.cil.c" % self.projectConfig.locationTempNoExtension + + logger.info("Preprocessing the file: merging other source files...") + + if merger.runMerger(self.projectConfig): + errMsg = "Error running the merger." + raise GameTimeError(errMsg) + else: + shutil.copyfile(cilFile, preprocessedFile) + shutil.move(cilFile, + "%s%s.c" % (self.projectConfig.locationTempNoExtension, + config.TEMP_SUFFIX_MERGED)) + if not self.projectConfig.debugConfig.KEEP_CIL_TEMPS: + cilHelper.removeTempCilFiles(self.projectConfig) + + logger.info("") + logger.info("Other source files merged.") + + def _runLoopUnroller(self): + """As part of preprocessing, runs CIL on the source file under analysis to unroll loops. A copy of the file that results from the CIL preprocessing is made and renamed for use by other preprocessing phases, and the file itself is renamed and stored for later perusal. - - Parameters: - compiled_file: str : - Path to the original file. - - Returns: - Path to unrolled file. """ - preprocessed_file: str = self.project_config.location_temp_file - + preprocessedFile = self.projectConfig.locationTempFile # Infer the name of the file that results from the CIL preprocessing. - unrolled_file: str = unroller.unroll(compiled_file, self.project_config.location_temp_dir, - f"{self.project_config.name_orig_no_extension}") + cilFile = "%s.cil.c" % self.projectConfig.locationTempNoExtension logger.info("Preprocessing the file: unrolling loops in the code...") - if not unrolled_file: - err_msg = "Error running the loop unroller." - raise GameTimeError(err_msg) + if loopHandler.runUnroller(self.projectConfig): + errMsg = "Error running the loop unroller." + raise GameTimeError(errMsg) else: - shutil.copyfile(unrolled_file, preprocessed_file) - + shutil.copyfile(cilFile, preprocessedFile) + shutil.move(cilFile, + "%s%s.c" % (self.projectConfig.locationTempNoExtension, + config.TEMP_SUFFIX_UNROLLED)) + if not self.projectConfig.debugConfig.KEEP_CIL_TEMPS: + cilHelper.removeTempCilFiles(self.projectConfig) + logger.info("") logger.info("Loops in the code have been unrolled.") - return unrolled_file - - def _run_inliner(self, input_file: str, additional_files: str): - """ - As part of preprocessing, runs CIL on the source file under + def _runInliner(self): + """As part of preprocessing, runs CIL on the source file under analysis to inline functions. A copy of the file that results from the CIL preprocessing is made and renamed for use by other preprocessing phases, and the file itself is renamed and stored for later perusal. - - Parameters: - input_file: str : - Path to input file. - - Returns: - Path to inlined file. """ - preprocessed_file = self.project_config.location_temp_file + preprocessedFile = self.projectConfig.locationTempFile # Infer the name of the file that results from the CIL preprocessing. + cilFile = "%s.cil.c" % self.projectConfig.locationTempNoExtension logger.info("Preprocessing the file: inlining...") - # inlined_file = clang_helper.inline_functions(input_file, self.project_config.location_temp_dir, - # f"{self.project_config.name_orig_no_extension}gt-inlined") - - input_files = [input_file] + additional_files - inlined_file = inliner.inline_functions(input_files, self.project_config.location_temp_dir, - f"{self.project_config.name_orig_no_extension}", self.project_config.func) - - if not inlined_file: - err_msg = "Error running the inliner." - raise GameTimeError(err_msg) + if inliner.runInliner(self.projectConfig): + errMsg = "Error running the inliner." + raise GameTimeError(errMsg) else: - shutil.copyfile(inlined_file, preprocessed_file) + shutil.copyfile(cilFile, preprocessedFile) + shutil.move(cilFile, + "%s%s.c" % (self.projectConfig.locationTempNoExtension, + config.TEMP_SUFFIX_INLINED)) + if not self.projectConfig.debugConfig.KEEP_CIL_TEMPS: + cilHelper.removeTempCilFiles(self.projectConfig) logger.info("") logger.info("Inlining complete.") - return inlined_file - - ### GRAPH FUNCTIONS ### - def create_dag(self): - """ - Creates the DAG corresponding to the code being analyzed - and dumps the DAG, in DOT format, to a temporary file for further - analysis. This method also stores a local copy in a data - structure that represents the DAG. + def _runCil(self): + """As part of preprocessing, runs CIL on the source file under + analysis to to reduce the C file to the subset of constructs + used by CIL for ease of analysis. The file that results from + the CIL preprocessing is renamed for use by the rest of + the GameTime toolflow. Another copy, with preprocessor directives + that maintain the line numbers from the original source file + (and other merged source files), is also made. """ - logger.info("Generating the DAG and associated information...") + preprocessedFile = self.projectConfig.locationTempFile + # Infer the name of the file that results from the CIL preprocessing. + cilFile = "%s.cil.c" % self.projectConfig.locationTempNoExtension - #TODO: add back construction dag from filepath - # if nx_helper.construct_dag(self.dag_path): - # err_msg = "Error running the Phoenix program analyzer." - # raise GameTimeError(err_msg) + logger.info("Preprocessing the file: running CIL to produce code " + "simplified for analysis...") - location = os.path.join(self.project_config.location_temp_dir, - f".{self.project_config.func}.dot") - self.load_dag_from_dot_file(location) + if cilHelper.runCil(self.projectConfig, keepLineNumbers=True): + errMsg = "Error running CIL in the final preprocessing phase." + raise GameTimeError(errMsg) + else: + shutil.move(cilFile, + "%s%s.c" % (self.projectConfig.locationTempNoExtension, + config.TEMP_SUFFIX_LINE_NUMS)) + if not self.projectConfig.debugConfig.KEEP_CIL_TEMPS: + cilHelper.removeTempCilFiles(self.projectConfig) + + if cilHelper.runCil(self.projectConfig): + errMsg = "Error running CIL in the final preprocessing phase." + raise GameTimeError(errMsg) + else: + shutil.move(cilFile, preprocessedFile) + if not self.projectConfig.debugConfig.KEEP_CIL_TEMPS: + cilHelper.removeTempCilFiles(self.projectConfig) + logger.info("") + logger.info("Final preprocessing phase complete.") - bitcode = [] - for node in self.dag.nodes: - bitcode.append(self.dag.get_node_label(self.dag.nodes_indices[node])) - find_labels("".join(bitcode), self.project_config.location_temp_dir) - logger.info("All possible labels extracted.") + ### BASIS MATRIX FUNCTIONS ### + def _initBasisMatrix(self): + """Initializes the basis matrix.""" + self.basisMatrix = eye(self.pathDimension) + if self.projectConfig.RANDOMIZE_INITIAL_BASIS: + self._randomizeBasisMatrix() + def _randomizeBasisMatrix(self): + """Randomizes the rows of the basis matrix using + a Fisher-Yates shuffle. - # special case for single node dag - if self.dag.num_nodes == 1 and self.dag.num_edges == 0: - self.path_dimension = 1 - return + Precondition: The basis matrix has been initialized. + """ + for i in xrange(self.pathDimension, 0, -1): + j = random.randrange(i) + self._swapBasisMatrixRows(i-1, j) - num_edges_reduced = len(self.dag.edges_reduced) - self.path_dimension = self.dag.num_edges - self.dag.num_nodes + 2 - if num_edges_reduced != self.path_dimension: - err_msg = ("The number of non-special edges is different from the dimension of the path.") - raise GameTimeError(err_msg) + def _swapBasisMatrixRows(self, i, j): + """Swaps two rows of the basis matrix. - logger.info("DAG generated.") - - logger.info("The control-flow graph has %d nodes and %d edges, with at most %d possible paths." % - (self.dag.num_nodes, self.dag.num_edges, self.dag.num_paths)) - logger.info("There are at most %d possible basis paths." % self.path_dimension) - logger.info("") + @param i Index of one row to swap. + @param j Index of other row to swap. + """ + rowToSwapOut = self.basisMatrix[j] + rowToSwapIn = self.basisMatrix[i] + rowLen = len(rowToSwapOut) + + tempRowToSwapOut = [0] * rowLen + for k in xrange(rowLen): + tempRowToSwapOut[k] = rowToSwapOut[k] + for k in xrange(rowLen): + rowToSwapOut[k] = rowToSwapIn[k] + rowToSwapIn[k] = tempRowToSwapOut[k] + + def saveBasisMatrix(self, location=None): + """Saves the basis matrix to a file for future analysis. + + @param location Location of the file. If this is not provided, + the basis matrix will be stored in a temporary file located in + the temporary directory used by GameTime for its analysis. + """ + location = location or os.path.join(self.projectConfig.locationTempDir, + config.TEMP_BASIS_MATRIX) + try: + savetxt(location, self.basisMatrix, fmt="%01.1f") + except EnvironmentError as e: + errMsg = "Error saving the basis matrix: %s" % e + raise GameTimeError(errMsg) + + def loadBasisMatrix(self, location=None): + """Loads the basis matrix from a file. + + @param location Location of the file. If this is not provided, + the basis file will be loaded from a temporary file located in + the temporary directory used by GameTime for its analysis. + """ + location = location or os.path.join(self.projectConfig.locationTempDir, + config.TEMP_BASIS_MATRIX) + try: + self.basisMatrix = genfromtxt(location, delimiter=" ") + except EnvironmentError as e: + errMsg = "Error loading the basis matrix: %s" % e + raise GameTimeError(errMsg) - def load_dag_from_dot_file(self, location: str): + ### GRAPH FUNCTIONS ### + def createDag(self): + """Creates the DAG corresponding to the code being analyzed + and dumps the DAG, in DOT format, to a temporary file for further + analysis. This method also stores a local copy in a data + structure that represents the DAG. """ - Loads the DAG that corresponds to the code being analyzed from a DOT file. + logger.info("Generating the DAG and associated information...") - Parameters: - location: str : - Location of the file. + if phoenixHelper.createDag(self.projectConfig): + errMsg = "Error running the Phoenix program analyzer." + raise GameTimeError(errMsg) - """ - self.dag, modified = nx_helper.construct_dag(location) - if modified: - modified_dag_location = os.path.join(self.project_config.location_temp_dir, - f".{self.project_config.func}_modified.dot") - write_dag_to_dot_file(self.dag, modified_dag_location) - logger.info("New CFG outputed to folder.") + location = os.path.join(self.projectConfig.locationTempDir, + config.TEMP_DAG) + self.loadDagFromDotFile(location) - # Reset variables of this "Analyzer" object. - self.reset_path_exclusive_constraints() - self.reset_path_bundled_constraints() + numEdgesReduced = len(self.dag.edgesReduced) + self.pathDimension = self.dag.numEdges - self.dag.numNodes + 2 + if numEdgesReduced != self.pathDimension: + errMsg = ("The number of non-special edges is different " + "from the dimension of the path.") + raise GameTimeError(errMsg) + logger.info("DAG generated.") - ### BASIS MATRIX FUNCTIONS ### - def _init_basis_matrix(self): - """Initializes the basis matrix.""" - self.basis_matrix: np.ndarray = eye(self.path_dimension) - if self.project_config.RANDOMIZE_INITIAL_BASIS: - self._randomize_basis_matrix() + if nxHelper.hasCycles(self.dag): + logger.warn("The control-flow graph has cycles.") + self._runLoopDetector() + else: + logger.info("The control-flow graph has %d nodes and %d edges, " + "with at most %d possible paths." % + (self.dag.numNodes, self.dag.numEdges, + self.dag.numPaths)) + logger.info("There are at most %d possible basis paths." % + self.pathDimension) + logger.info("") - def _randomize_basis_matrix(self): - """ - Randomizes the rows of the basis matrix using - a Fisher-Yates shuffle. - - Precondition: The basis matrix has been initialized. - """ - for i in range(self.path_dimension, 0, -1): - j = np.random.randint(i) - self._swap_basis_matrix_rows(i - 1, j) + def loadDagFromDotFile(self, location): + """Loads the DAG that corresponds to the code being analyzed + from a DOT file. - def _swap_basis_matrix_rows(self, i, j): + @param location Location of the file. """ - Swaps two rows of the basis matrix. + self.dag = nxHelper.constructDag(location) - Parameters: - i: - Index of one row to swap. - j: - Index of other row to swap. + # Reset variables of this "Analyzer" object. + self.resetPathExclusiveConstraints() + self.resetPathBundledConstraints() + + def writeDagToDotFile(self, location=None, annotateEdges=False, + highlightedPath=None, highlightColor="red"): + """Writes the DAG that corresponds to the code being analyzed + to a DOT file. + + @param location Location of the file. If this is not provided, + the basis matrix will be stored in a temporary file located in + the temporary directory used by GameTime for its analysis. + @param annotateEdges Whether each edge should be annotated with + its weight, when the file is processed by a visualization tool. + @param highlightedPath "Path" object whose corresponding edges + will be highlighted when the DOT file is processed by + a visualization tool. If this argument is not provided, no edges + will be highlighted. + @param highlightColor Color of the highlighted edges. This argument + can be any value that is legal in the DOT format; by default, its value + is "red". If the "highlightedPath" argument is not provided, + this argument is ignored. """ - row_to_swap_out = self.basis_matrix[j] - row_to_swap_in = self.basis_matrix[i] - row_len = len(row_to_swap_out) + location = location or os.path.join(self.projectConfig.locationTempDir, + config.TEMP_DAG_WEIGHTS) + edgeWeights = [("%g" % edgeWeight) for edgeWeight + in self.dag.edgeWeights] + edgesToWeights = (dict(zip(self.dag.allEdges, edgeWeights)) + if annotateEdges else None) + nxHelper.writeDagToDotFile(self.dag, location, + self.projectConfig.func, edgesToWeights, + (Dag.getEdges(highlightedPath.nodes) + if highlightedPath else None), + highlightColor) + + def _runLoopDetector(self): + """Runs the loop detector on the code under analysis.""" + logger.info("Detecting loops in the code...") + + if loopHandler.runDetector(self.projectConfig): + errMsg = "Error running the loop detector." + raise GameTimeError(errMsg) + else: + if not self.projectConfig.debugConfig.KEEP_CIL_TEMPS: + cilHelper.removeTempCilFiles(self.projectConfig) - temp_row_to_swap_out = [0] * row_len - for k in range(row_len): - temp_row_to_swap_out[k] = row_to_swap_out[k] - for k in range(row_len): - row_to_swap_out[k] = row_to_swap_in[k] - row_to_swap_in[k] = temp_row_to_swap_out[k] + logger.info("") + logger.info("Loops in the code have been detected.") + logger.info("Before proceeding, please modify the loop configuration " + "file in the temporary directory generated by GameTime " + "for this analysis, and then run the loop unroller " + "to unroll these loops.") + + def _compressPath(self, pathEdges): + """Compresses the path provided: this method converts + the provided path to a 0-1 vector that is 1 if a + 'non-special' edge is along the path, and 0 otherwise. + @param pathEdges Edges along the path to represent with + 'non-special' edges. + @retval 0-1 vector that is 1 if a `non-special' edge is along + the path, and 0 otherwise. + """ + return [(1.0 if edge in pathEdges else 0.0) + for edge in self.dag.edgesReduced] ### PATH GENERATION FUNCTIONS ### - def add_path_exclusive_constraint(self, edges: List[Tuple[str, str]]): - """ - Adds the edges provided to the list of path-exclusive + def addPathExclusiveConstraint(self, edges): + """Adds the edges provided to the list of path-exclusive constraints, if not already present. These edges must not be taken together along any path through the DAG. - Parameters: - edges: List[Tuple[str, str]] : - List of edges to add to the list of path-exclusive constraints. - + @param edges List of edges to add to the list of + path-exclusive constraints. """ - if edges not in self.path_exclusive_constraints: - self.path_exclusive_constraints.append(edges) + if edges not in self.pathExclusiveConstraints: + self.pathExclusiveConstraints.append(edges) - def add_path_bundled_constraint(self, edges: List[Tuple[str, str]]): - """ - Adds the edges provided to the list of path-bundled + def addPathBundledConstraint(self, edges): + """Adds the edges provided to the list of path-bundled constraints, if not already present. These edges must be taken together if at least one of them is taken along a path through the DAG. - - Parameters: - edges: List[Tuple[str, str]] : - List of edges to add to the list of path-bundled constraints. + @param edges List of edges to add to the list of path-bundled + constraints. """ - if edges not in self.path_bundled_constraints: - self.path_bundled_constraints.append(edges) + if edges not in self.pathBundledConstraints: + self.pathBundledConstraints.append(edges) - def reset_path_exclusive_constraints(self): + def resetPathExclusiveConstraints(self): """Resets the path-exclusive constraints.""" - self.path_exclusive_constraints = [] + self.pathExclusiveConstraints = [] - def reset_path_bundled_constraints(self): + def resetPathBundledConstraints(self): """Resets the path-bundled constraints.""" - self.path_bundled_constraints = [] - - def _compress_path(self, path_edges: List[Tuple[str, str]]) -> List[float]: - """ - Compresses the path provided: this method converts - the provided path to a 0-1 vector that is 1 if a - 'non-special' edge is along the path, and 0 otherwise. - - Parameters: - path_edges: List[Tuple[str, str]] : - Edges along the path to represent with 'non-special' edges. + self.pathBundledConstraints = [] - Returns: - 0-1 vector that is 1 if a `non-special' edge is along the path, and 0 otherwise. - """ - return [(1.0 if edge in path_edges else 0.0) - for edge in self.dag.edges_reduced] - - ####### Fuctions to FIX - def generate_overcomplete_basis(self, k: int): - """ - Generates an overcomplete basis so that each feasible path can be + def generateOvercompleteBasis(self, k): + """Generates an overcomplete basis so that each feasible path can be written as a liner combination of the paths in the basis so that the L1 norm is at most 'k'. This method is for testing purposes - only as it exhaustively generates all_temp_files paths in the graph!. Use the + only as it exhaustively generates all paths in the graph!. Use the function below for a scalable version. - - Parameters: - k: int : - Maximum value of L1 norm. - """ - logger.info("Generating all_temp_files paths") + logger.info("Generating all paths") paths = nx.all_simple_paths(self.dag, self.dag.source, self.dag.sink) feasible = list(paths) logger.info("Find minimal overcomplete basis") - pulp_helper.find_minimal_overcomplete_basis(self, feasible, k) - - - def iteratively_find_overcomplete_basis(self, initial_paths: List[List[Tuple[str, str]]], k: int): - """ - Generates overcomplete basis such the lenth of the longest - feasible path is at most 'k'. The basis is computed by iteratively - extending the basis with the longest path. Parameter 'initial_paths' - specifies the set of paths the iterative algorithm begins with. This - can be any set of paths, in practice we use the paths generated by - the standard algorithm. - - Parameters: - initial_paths: List[List[Tuple[str, str]]] : - A list of initial paths to begin with. - - k: int : - Maximum value of L1 norm. - - Returns: - The set of basis paths. + pulpHelper.findMinimalOvercompleteBasis(self, feasible, k) + + def iterativelyFindOvercompleteBasis(self, initialPaths, k): + """Generates overcomplete basis such the the lenth of the longest + feasible path is at most 'k'. The basis is computed by iteratively + extending the basis with the longest path. Parameter 'initialPaths' + specifies the set of paths the iterative algorithm begins with. This + can be any set of paths, in practice we use the paths generated by + the standard algorithm. """ infeasible = [] - edge_node_paths = initial_paths - optimal_bound = 1 - start_time = time.perf_counter() + edgeNodePaths = initialPaths + optimalBound = 1 + startTime = time.clock() while True: - before_time = time.perf_counter() - length, path, ilp_problem = \ - pulp_helper.find_worst_expressible_path(self, self.basis_paths, 0) - after_time = time.perf_counter() + beforeTime = time.clock() + length, path, ilpProblem = \ + pulpHelper.findWorstExpressiblePath(self, self.basisPaths, 0) + afterTime = time.clock() logger.info("Found a candidate path of length %.2f in %d seconds" % - (length, after_time - before_time)) + (length, afterTime - beforeTime)) - optimal_bound = length + optimalBound = length # if the length of the longest path is within the given bound, stop - if length <= k: break + if (length <= k): break - candidate_path_nodes = path - candidate_path_edges = Dag.get_edges(candidate_path_nodes) + candidatePathNodes = path + candidatePathEdges = Dag.getEdges(candidatePathNodes) logger.info("Checking if the found path is feasible...") - result_path = Path(ilp_problem=ilp_problem, nodes=candidate_path_nodes) - value = self.measure_path(result_path) - if value < float('inf'): + resultPath = self.checkFeasibility(candidatePathNodes, + ilpProblem) + querySatisfiability = resultPath.smtQuery.satisfiability + if querySatisfiability == Satisfiability.SAT: logger.info("Path is feasible.") - self.basis_paths.append(result_path) - edge_node_paths.append(candidate_path_edges) - else: + self.basisPaths.append(resultPath) + edgeNodePaths.append(candidatePathEdges) + elif querySatisfiability == Satisfiability.UNSAT: logger.info("Path is infeasible.") logger.info("Finding the edges to exclude...") - infeasible.append(candidate_path_edges) - unsat_core = result_path.smtQuery.unsatCore - exclude_edges = result_path.get_edges_for_conditions(unsat_core) + infeasible.append(candidatePathEdges) + unsatCore = resultPath.smtQuery.unsatCore + excludeEdges = resultPath.getEdgesForConditions(unsatCore) logger.info("Edges to be excluded found.") logger.info("Adding a constraint to exclude " "these edges...") - if len(exclude_edges) > 0: - self.add_path_exclusive_constraint(exclude_edges) + if len(excludeEdges) > 0: + self.addPathExclusiveConstraint(excludeEdges) else: - self.add_path_exclusive_constraint(candidate_path_edges) + self.addPathExclusiveConstraint(candidatePathEdges) logger.info("Constraint added.") - + logger.info("Found overcomplete basis of size %d, yielding bound %.2f" % - (len(edge_node_paths), optimal_bound)) - - self.basis_paths_nodes = [path.nodes for path in self.basis_paths] - return self.basis_paths + (len(edgeNodePaths), optimalBound)) + + self.basisPathsNodes = [path.nodes for path in self.basisPaths] + return self.basisPaths - def generate_basis_paths(self): - """ - Generates a list of "Path" objects, each of which represents + def generateBasisPaths(self): + """Generates a list of "Path" objects, each of which represents a basis path of the code being analyzed. The basis "Path" objects are regenerated each time this method is called. - Returns: - List of basis paths of the code being analyzed, each - represented by an object of the "Path" class. + @retval List of basis paths of the code being analyzed, each + represented by an object of the "Path" class. """ - basis_paths = [] + basisPaths = [] - if nx_helper.has_cycles(self.dag): - logger.warning("Loops in the code have been detected.") - # logger.warning("No basis paths have been generated.") + if nxHelper.hasCycles(self.dag): + logger.warn("Loops in the code have been detected.") + logger.warn("No basis paths have been generated.") return [] logger.info("Generating the basis paths...") logger.info("") - start_time = time.perf_counter() + startTime = time.clock() logger.info("Initializing the basis matrix...") - self._init_basis_matrix() + self._initBasisMatrix() logger.info("Basis matrix initialized to") - logger.info(self.basis_matrix) + logger.info(self.basisMatrix) logger.info("") logger.info("There are a maximum of %d possible basis paths." % - self.path_dimension) + self.pathDimension) logger.info("") - def on_exit(start_time, infeasible): - """ - Helper function that is called when this method is about to + def onExit(startTime, infeasible): + """Helper function that is called when this method is about to return the basis Path objects, and performs the appropriate pre-exit cleanup. This inner function will be used in two places below, and is defined once to keep the code neat, to prevent deeper indentation, and to reduce confusion. - Parameters: - start_time : - Time when the generation of basis Path objects was started. - infeasible : - Set of infeasible paths. - - Returns: - List of basis paths of the code being analyzed, each represented by an object of the Path class. + @param startTime Time when the generation of basis Path objects + was started. + @retval List of basis paths of the code being analyzed, each + represented by an object of the Path class. """ - self.basis_paths = basis_paths - self.basis_paths_nodes = [path.nodes for path in basis_paths] - # self.resetPathExclusiveConstraints() + self.basisPaths = basisPaths + self.basisPathsNodes = [path.nodes for path in basisPaths] + #self.resetPathExclusiveConstraints() logger.info("Time taken to generate paths: %.2f seconds." % - (time.perf_counter() - start_time)) + (time.clock() - startTime)) logger.info("Basis paths generated.") - + # If we are computing overcomplete basis, use the computed set as # the initial set of paths in the iterative algorithm, - if self.project_config.OVER_COMPLETE_BASIS: + if self.projectConfig.OVER_COMPLETE_BASIS: logger.info("Iteratively improving the basis") for path in infeasible: - self.add_path_exclusive_constraint(path) - edge_paths = \ - [Dag.get_edges(path.nodes) for path in self.basis_paths] - result = self.iteratively_find_overcomplete_basis( - edge_paths, self.project_config.MAXIMUM_ERROR_SCALE_FACTOR) + self.addPathExclusiveConstraint(path) + edgePaths = \ + [Dag.getEdges(path.nodes) for path in self.basisPaths] + result = self.iterativelyFindOvercompleteBasis( + edgePaths, self.projectConfig.MAXIMUM_ERROR_SCALE_FACTOR) logger.info("Number of paths generated: %d" % len(result)) logger.info("Time taken to generate paths: %.2f seconds." % - (time.perf_counter() - start_time)) + (time.clock() - startTime)) return result else: - return self.basis_paths - - if self.path_dimension == 1: - warn_msg = ("Basis matrix has dimensions 1x1. " - "There is only one path through the function " - "under analysis, which is the only basis path.") - logger.warning(warn_msg) - - if self.dag.num_nodes == 1 and self.dag.num_edges == 0: - warn_msg = "Single node CFD with no edge. Only one possible path." - logger.warning(warn_msg) - basis_paths = [Path(nodes=[self.dag.source])] - return on_exit(start_time, []) - - i = 0 + return self.basisPaths - # Collects all_temp_files infeasible paths discovered during the computation + if self.pathDimension == 1: + warnMsg = ("Basis matrix has dimensions 1x1. " + "There is only one path through the function " + "under analysis, which is the only basis path.") + logger.warn(warnMsg) + + # Collects all infeasible paths discovered during the computation infeasible = [] - current_row, num_paths_unsat = 0, 0 - while current_row < (self.path_dimension - self.num_bad_rows): - logger.info("Currently at row %d..." % (current_row + 1)) - logger.info("So far, the bottom %d rows of the basis matrix are `bad'." % self.num_bad_rows) - logger.info("So far, %d candidate paths were found to be unsatisfiable." % num_paths_unsat) - logger.info(f"Basis matrix is {self.basis_matrix}") + currentRow, numPathsUnsat = 0, 0 + while currentRow < (self.pathDimension - self.numBadRows): + logger.info("Currently at row %d..." % (currentRow+1)) + logger.info("So far, the bottom %d rows of the basis " + "matrix are `bad'." % self.numBadRows) + logger.info("So far, %d candidate paths were found to be " + "unsatisfiable." % numPathsUnsat) + logger.info("Basis matrix is") + logger.info(self.basisMatrix) logger.info("") logger.info("Calculating subdeterminants...") - if num_paths_unsat == 0: - # Calculate the subdeterminants only if the replacement of this row has not yet been attempted. - self.dag.reset_edge_weights() - self.dag.edge_weights = self._calculate_subdets(current_row) + if numPathsUnsat == 0: + # Calculate the subdeterminants only if the replacement + # of this row has not yet been attempted. + self.dag.resetEdgeWeights() + self.dag.edgeWeights = self._calculateSubdets(currentRow) logger.info("Calculation complete.") - logger.info("Finding a candidate path using an integer linear program...") + logger.info("Finding a candidate path using an integer " + "linear program...") logger.info("") - candidate_path_nodes, ilp_problem = pulp_helper.find_extreme_path(self) + candidatePathNodes, ilpProblem = pulpHelper.findExtremePath(self) logger.info("") - if ilp_problem.obj_val is None: - logger.info("Unable to find a candidate path to replace row %d." % (current_row + 1)) - logger.info("Moving the bad row to the bottom of the basis matrix.") - for k in range((current_row + 1), self.path_dimension): - self._swap_basis_matrix_rows(k - 1, k) - self.num_bad_rows += 1 - num_paths_unsat = 0 + if ilpProblem.objVal is None: + logger.info("Unable to find a candidate path to " + "replace row %d." % (currentRow+1)) + logger.info("Moving the bad row to the bottom " + "of the basis matrix.") + for k in xrange((currentRow+1), self.pathDimension): + self._swapBasisMatrixRows(k-1, k) + self.numBadRows += 1 + numPathsUnsat = 0 continue - logger.info("Candidate path found.") - - candidate_path_edges = Dag.get_edges(candidate_path_nodes) - compressed_path = self._compress_path(candidate_path_edges) - # Temporarily replace the row in the basis matrix to calculate the new determinant. - prev_matrix_row = self.basis_matrix[current_row].copy() - self.basis_matrix[current_row] = compressed_path - sign, new_basis_matrix_log_det = slogdet(self.basis_matrix) - new_basis_matrix_det = exp(new_basis_matrix_log_det) - logger.info("Absolute value of the new determinant: %g" % new_basis_matrix_det) + logger.info("Candidate path found.") + candidatePathEdges = Dag.getEdges(candidatePathNodes) + compressedPath = self._compressPath(candidatePathEdges) + + # Temporarily replace the row in the basis matrix + # to calculate the new determinant. + prevMatrixRow = self.basisMatrix[currentRow].copy() + self.basisMatrix[currentRow] = compressedPath + sign, newBasisMatrixLogDet = slogdet(self.basisMatrix) + newBasisMatrixDet = exp(newBasisMatrixLogDet) + logger.info("Absolute value of the new determinant: %g" % + newBasisMatrixDet) logger.info("") - DETERMINANT_THRESHOLD = self.project_config.DETERMINANT_THRESHOLD - MAX_INFEASIBLE_PATHS = self.project_config.MAX_INFEASIBLE_PATHS - if ((sign == 0 and new_basis_matrix_log_det == float("-inf")) or - new_basis_matrix_det < DETERMINANT_THRESHOLD or - num_paths_unsat >= MAX_INFEASIBLE_PATHS): # If row is bad - - if (new_basis_matrix_det < DETERMINANT_THRESHOLD and not (sign == 0 and new_basis_matrix_log_det == float("-inf"))): + DETERMINANT_THRESHOLD = self.projectConfig.DETERMINANT_THRESHOLD + MAX_INFEASIBLE_PATHS = self.projectConfig.MAX_INFEASIBLE_PATHS + if ((sign == 0 and newBasisMatrixLogDet == float("-inf")) or + newBasisMatrixDet < DETERMINANT_THRESHOLD or + numPathsUnsat >= MAX_INFEASIBLE_PATHS): + if (newBasisMatrixDet < DETERMINANT_THRESHOLD and + not (sign == 0 and newBasisMatrixLogDet == float("-inf"))): logger.info("Determinant is too small.") else: - logger.info("Unable to find a path that makes the determinant non-zero.") - - logger.info("Moving the bad row to the bottom of the basis matrix.") - - self.basis_matrix[current_row] = prev_matrix_row - for k in range((current_row + 1), self.path_dimension): - self._swap_basis_matrix_rows(k - 1, k) - self.num_bad_rows += 1 - num_paths_unsat = 0 - else: # Row is good, check feasibility + logger.info("Unable to find a path that makes " + "the determinant non-zero.") + logger.info("Moving the bad row to the bottom " + "of the basis matrix.") + self.basisMatrix[currentRow] = prevMatrixRow + for k in xrange((currentRow+1), self.pathDimension): + self._swapBasisMatrixRows(k-1, k) + self.numBadRows += 1 + numPathsUnsat = 0 + else: logger.info("Possible replacement for row found.") logger.info("Checking if replacement is feasible...") logger.info("") - result_path = Path(ilp_problem=ilp_problem, nodes=candidate_path_nodes) - - # feasibility test - value = self.measure_path(result_path, f'gen-basis-path-row{current_row}-attempt{i}') - i += 1 - if value < float('inf'): + resultPath = self.checkFeasibility(candidatePathNodes, + ilpProblem) + querySatisfiability = resultPath.smtQuery.satisfiability + if querySatisfiability == Satisfiability.SAT: # Sanity check: - # A row should not be replaced if it replaces a good row and decreases the determinant. However, replacing a bad row and decreasing the determinant is okay. (TODO: Are we actually doing this?) + # A row should not be replaced if it replaces a good + # row and decreases the determinant. However, + # replacing a bad row and decreasing the determinant + # is okay. (TODO: Are we actually doing this?) logger.info("Replacement is feasible.") - logger.info("Row %d replaced." % (current_row + 1)) - basis_paths.append(result_path) - current_row += 1 - num_paths_unsat = 0 - else: + logger.info("Row %d replaced." % (currentRow+1)) + + basisPaths.append(resultPath) + currentRow += 1 + numPathsUnsat = 0 + elif querySatisfiability == Satisfiability.UNSAT: logger.info("Replacement is infeasible.") - logger.info("Adding a constraint to exclude these edges...") - self.add_path_exclusive_constraint(candidate_path_edges) - infeasible.append(candidate_path_edges) + logger.info("Finding the edges to exclude...") + unsatCore = resultPath.smtQuery.unsatCore + excludeEdges = resultPath.getEdgesForConditions(unsatCore) + logger.info("Edges to be excluded found.") + logger.info("Adding a constraint to exclude " + "these edges...") + if len(excludeEdges) > 0: + self.addPathExclusiveConstraint(excludeEdges) + infeasible.append(excludeEdges) + else: + self.addPathExclusiveConstraint(candidatePathEdges) + infeasible.append(candidatePathEdges) logger.info("Constraint added.") - self.basis_matrix[current_row] = prev_matrix_row - num_paths_unsat += 1 + + self.basisMatrix[currentRow] = prevMatrixRow + numPathsUnsat += 1 logger.info("") logger.info("") - if self.project_config.PREVENT_BASIS_REFINEMENT: - return on_exit(start_time, infeasible) + if self.projectConfig.PREVENT_BASIS_REFINEMENT: + return onExit(startTime, infeasible) logger.info("Refining the basis into a 2-barycentric spanner...") logger.info("") - is_two_barycentric = False - refinement_round = 0 - while not is_two_barycentric: - logger.info("Currently in round %d of refinement..." % (refinement_round + 1)) + isTwoBarycentric = False + refinementRound = 0 + while not isTwoBarycentric: + logger.info("Currently in round %d of refinement..." % + (refinementRound+1)) logger.info("") - is_two_barycentric = True - current_row, num_paths_unsat = 0, 0 - good_rows = (self.path_dimension - self.num_bad_rows) - while current_row < good_rows: - logger.info("Currently at row %d out of %d..." % (current_row + 1, good_rows)) - logger.info("So far, %d candidate paths were found to be unsatisfiable." % num_paths_unsat) - logger.info(f"Basis matrix is {self.basis_matrix}") + isTwoBarycentric = True + currentRow, numPathsUnsat = 0, 0 + goodRows = (self.pathDimension - self.numBadRows) + while currentRow < goodRows: + logger.info("Currently at row %d out of %d..." % + (currentRow+1, goodRows)) + logger.info("So far, %d candidate paths were found to be " + "unsatisfiable." % numPathsUnsat) + logger.info("Basis matrix is") + logger.info(self.basisMatrix) logger.info("") logger.info("Calculating subdeterminants...") - if num_paths_unsat == 0: - # Calculate the subdeterminants only if the replacement of this row has not yet been attempted. - self.dag.reset_edge_weights() - self.dag.edge_weights = self._calculate_subdets(current_row) + if numPathsUnsat == 0: + # Calculate the subdeterminants only if the replacement + # of this row has not yet been attempted. + self.dag.resetEdgeWeights() + self.dag.edgeWeights = self._calculateSubdets(currentRow) logger.info("Calculation complete.") - logger.info("Finding a candidate path using an integer linear program...") + logger.info("Finding a candidate path using an integer " + "linear program...") logger.info("") - candidate_path_nodes, ilp_problem = pulp_helper.find_extreme_path(self) + candidatePathNodes, ilpProblem = \ + pulpHelper.findExtremePath(self) logger.info("") - if ilp_problem.obj_val is None: - logger.info("Unable to find a candidate path to replace row %d." % (current_row + 1)) - current_row += 1 - num_paths_unsat = 0 + if ilpProblem.objVal is None: + logger.info("Unable to find a candidate path to " + "replace row %d." % (currentRow+1)) + currentRow += 1 + numPathsUnsat = 0 continue logger.info("Candidate path found.") - candidate_path_edges = Dag.get_edges(candidate_path_nodes) - compressed_path = self._compress_path(candidate_path_edges) + candidatePathEdges = Dag.getEdges(candidatePathNodes) + compressedPath = self._compressPath(candidatePathEdges) - sign, old_basis_matrix_log_det = slogdet(self.basis_matrix) - old_basis_matrix_det = exp(old_basis_matrix_log_det) - logger.info("Absolute value of the old determinant: %g" % old_basis_matrix_det) + sign, oldBasisMatrixLogDet = slogdet(self.basisMatrix) + oldBasisMatrixDet = exp(oldBasisMatrixLogDet) + logger.info("Absolute value of the old determinant: %g" % + oldBasisMatrixDet) # Temporarily replace the row in the basis matrix # to calculate the new determinant. - prev_matrix_row = self.basis_matrix[current_row].copy() - self.basis_matrix[current_row] = compressed_path - sign, new_basis_matrix_log_det = slogdet(self.basis_matrix) - new_basis_matrix_det = exp(new_basis_matrix_log_det) - logger.info("Absolute value of the new determinant: %g" % new_basis_matrix_det) - - if new_basis_matrix_det > 2 * old_basis_matrix_det: + prevMatrixRow = self.basisMatrix[currentRow].copy() + self.basisMatrix[currentRow] = compressedPath + sign, newBasisMatrixLogDet = slogdet(self.basisMatrix) + newBasisMatrixDet = exp(newBasisMatrixLogDet) + logger.info("Absolute value of the new determinant: %g" % + newBasisMatrixDet) + + if newBasisMatrixDet > 2 * oldBasisMatrixDet: logger.info("Possible replacement for row found.") logger.info("Checking if replacement is feasible...") logger.info("") - result_path = Path(ilp_problem=ilp_problem, nodes=candidate_path_nodes) - basis_paths[current_row] = result_path - current_row += 1 - num_paths_unsat = 0 - - #feasibility test - value = self.measure_path(result_path, f'gen-basis-path-replace-candid-{current_row+1}-{good_rows}') - - if value < float('inf'): + resultPath = self.checkFeasibility(candidatePathNodes, + ilpProblem) + querySatisfiability = resultPath.smtQuery.satisfiability + if querySatisfiability == Satisfiability.SAT: logger.info("Replacement is feasible.") - is_two_barycentric = False - basis_paths[current_row] = result_path - logger.info("Row %d replaced." % (current_row + 1)) - current_row += 1 - num_paths_unsat = 0 - else: + isTwoBarycentric = False + basisPaths[currentRow] = resultPath + logger.info("Row %d replaced." % (currentRow+1)) + + currentRow += 1 + numPathsUnsat = 0 + elif querySatisfiability == Satisfiability.UNSAT: logger.info("Replacement is infeasible.") - self.add_path_exclusive_constraint(candidate_path_edges) - logger.info("Adding a constraint to exclude these edges...") - infeasible.append(candidate_path_edges) + + logger.info("Finding the edges to exclude...") + unsatCore = resultPath.smtQuery.unsatCore + excludeEdges = \ + resultPath.getEdgesForConditions(unsatCore) + logger.info("Edges to be excluded found.") + logger.info("Adding a constraint to exclude " + "these edges...") + if len(excludeEdges) > 0: + self.addPathExclusiveConstraint(excludeEdges) + infeasible.append(excludeEdges) + else: + self.addPathExclusiveConstraint(candidatePathEdges) + infeasible.append(candidatePathEdges) logger.info("Constraint added.") - self.basis_matrix[current_row] = prev_matrix_row - num_paths_unsat += 1 + self.basisMatrix[currentRow] = prevMatrixRow + numPathsUnsat += 1 else: - logger.info("No replacement for row %d found." % (current_row + 1)) - self.basis_matrix[current_row] = prev_matrix_row - current_row += 1 - num_paths_unsat = 0 + logger.info("No replacement for row %d found." % + (currentRow+1)) + self.basisMatrix[currentRow] = prevMatrixRow + currentRow += 1 + numPathsUnsat = 0 logger.info("") logger.info("") - refinement_round += 1 + refinementRound += 1 logger.info("") logger.info("Basis refined.") - return on_exit(start_time, infeasible) + return onExit(startTime, infeasible) + + # Methods imported from the "PathGenerator" class. + def generatePaths(self, *args, **kwargs): + return PathGenerator.generatePaths(self, *args, **kwargs) ### PATH GENERATION HELPER FUNCTIONS ### - def _calculate_subdets(self, row: int) -> List[int]: - """ - Returns a list of weights, where weight i is assigned to + def _calculateSubdets(self, row): + """Returns a list of weights, where weight i is assigned to edge i. The weights assigned to the `non-special' edges are subdeterminants of the basis matrix without row i and column j: column j corresponds to the `non-special' edge j. - Parameters: - row: int : - Row to ignore. - - Returns: - List of weights as specified above. + @param row Row to ignore. + @retval List of weights as specified above. """ - edges_reduced = self.dag.edges_reduced - edges_reduced_indices = self.dag.edges_reduced_indices + edgesReduced = self.dag.edgesReduced + edgesReducedIndices = self.dag.edgesReducedIndices - edge_weight_list = [0] * self.dag.num_edges + edgeWeightList = [0] * self.dag.numEdges - row_list = list(range(self.path_dimension)) - row_list.remove(row) + rowList = range(self.pathDimension) + rowList.remove(row) - for j in range(self.path_dimension): - col_list = list(range(self.path_dimension)) - col_list.remove(j) - sub_matrix = self.basis_matrix[row_list][:, col_list] + for j in xrange(self.pathDimension): + colList = range(self.pathDimension) + colList.remove(j) + subMatrix = self.basisMatrix[rowList][:, colList] - if sub_matrix.size != 0: + if subMatrix.size != 0: # Compute the subdeterminant of this submatrix. - subdet = det(sub_matrix) - if ((row + j) % 2) == 1: - edge_weight = -1 * subdet + subdet = det(subMatrix) + if ((row+j) % 2) == 1: + edgeWeight = -1 * subdet else: - edge_weight = subdet + edgeWeight = subdet else: # Special case of a 1x1 matrix, or of code under analysis # with only one path that goes through. - edge_weight = 1 + edgeWeight = 1 # Assign this edge weight to the proper `non-special' edge. - edge_weight_list[edges_reduced_indices[edges_reduced[j]]] = edge_weight + edgeWeightList[edgesReducedIndices[edgesReduced[j]]] = edgeWeight + + return edgeWeightList + + def checkFeasibility(self, pathNodes, ilpProblem): + """Determines the feasibility of the provided path in the DAG; + the feasibility is checked with an SMT solver. This method + returns a Path object that contains, at least, a Query object + that represents the SMT query that contains the conditions along + the path provided; the feasibility of the path is the same as the + satisfiability of this Query object. If the path is feasible, + then the Path object also contains satisfying assignments. + + @param pathNodes Path whose feasibility should be checked, given + as a list of nodes along the path. + @param ilpProblem Integer linear programming problem that, when solved, + produced this path, represented as an IlpProblem object. + @retval Path object as described above. + """ + # First, check if the candidate path is already a basis path. + # This allows us to prevent unnecessary work. + # It is also a hack around a problem in Z3, where the same query + # can result in different models when checked more than once in + # the same execution. + # (See http://stackoverflow.com/q/15731179/1834042 for more details.) + logger.info("Checking if the candidate path is already " + "a basis path...") + try: + basisPathIndex = self.basisPathsNodes.index(pathNodes) + logger.info("Candidate path is a basis path.") + + # Create a copy of the Path object that represents the basis path: + # we do not want to modify the IlpProblem object associated with + # the basis Path object. + pathCopy = deepcopy(self.basisPaths[basisPathIndex]) + pathCopy.ilpProblem = ilpProblem + return pathCopy + except ValueError as e: + logger.info("Candidate path is not a basis path.") + + # Write the candidate path to a file for further analysis + # by the Phoenix backend. + logger.info("Writing nodes along candidate path to file...") + nodesFile = os.path.join(self.projectConfig.locationTempDir, + config.TEMP_PATH_NODES) + try: + nodesFileHandler = open(nodesFile, "w") + except EnvironmentError as e: + errMsg = "Error writing nodes along candidate path: %s" % e + raise GameTimeError(errMsg) + else: + with nodesFileHandler: + nodesFileHandler.write(" ".join(pathNodes)) + logger.info("Writing complete.") - return edge_weight_list + logger.info("Running the Phoenix program analyzer...") + logger.info("") + if phoenixHelper.findConditions(self.projectConfig): + errMsg = "Error running the Phoenix program analyzer." + raise GameTimeError(errMsg) + logger.info("Phoenix program analysis complete.") + logger.info("") + logger.info("Reading the line numbers of statements " + "along the path...") + lineNumbersFile = os.path.join(self.projectConfig.locationTempDir, + config.TEMP_PATH_LINE_NUMBERS) + lineNumbers = Path.readLineNumbersFromFile(lineNumbersFile) + logger.info("Line numbers of the statements along " + "the path read and processed.") + + logger.info("Reading the conditions along the path...") + conditionsFile = os.path.join(self.projectConfig.locationTempDir, + config.TEMP_PATH_CONDITIONS) + conditions = Path.readConditionsFromFile(conditionsFile) + logger.info("Path conditions read and processed.") + + logger.info("Reading the edges that are associated with " + "the conditions along the path...") + conditionEdgesFile = os.path.join(self.projectConfig.locationTempDir, + config.TEMP_PATH_CONDITION_EDGES) + conditionEdges = Path.readConditionEdgesFromFile(conditionEdgesFile) + logger.info("Edges read and processed.") + + logger.info("Reading the line numbers and truth values " + "of conditional points...") + conditionTruthsFile = os.path.join(self.projectConfig.locationTempDir, + config.TEMP_PATH_CONDITION_TRUTHS) + conditionTruths = Path.readConditionTruthsFromFile(conditionTruthsFile) + logger.info("Path condition truths read and processed.") + + logger.info("Reading information about array accesses...") + arrayAccessesFile = os.path.join(self.projectConfig.locationTempDir, + config.TEMP_PATH_ARRAY_ACCESSES) + arrayAccesses = Path.readArrayAccessesFromFile(arrayAccessesFile) + logger.info("Array accesses information read and processed.") + + logger.info("Reading information about the expressions " + "for aggregate accesses...") + aggIndexExprsFile = os.path.join(self.projectConfig.locationTempDir, + config.TEMP_PATH_AGG_INDEX_EXPRS) + aggIndexExprs = Path.readAggIndexExprsFromFile(aggIndexExprsFile) + logger.info("Aggregate accesses information read and processed.") + + logger.info("Reading the SMT query generated by the " + "Phoenix program analyzer...") + smtQueryFile = os.path.join(self.projectConfig.locationTempDir, + "%s.smt" % config.TEMP_PATH_QUERY) + smtQuery = readQueryFromFile(smtQueryFile) + logger.info("SMT query read.") + + assignments = {} + + logger.info("Checking the satisfiability of the SMT query...") + smtSolver = self.projectConfig.smtSolver + smtSolver.checkSat(smtQuery) + logger.info("Satisfiability checked.") + + if smtQuery.satisfiability == Satisfiability.SAT: + logger.info("Candidate path is FEASIBLE.") + + logger.info("Generating assignments...") + smtModelParser = self.projectConfig.smtModelParser + assignments = smtModelParser.parseModel(smtQuery.model, + arrayAccesses, + aggIndexExprs, + self.projectConfig) + logger.info("Assignments generated.") + elif smtQuery.satisfiability == Satisfiability.UNSAT: + logger.info("Candidate path is INFEASIBLE.") + elif smtQuery.satisfiability == Satisfiability.UNKNOWN: + errMsg = "Candidate path has UNKNOWN satisfiability." + raise GameTimeError(errMsg) + + if self.projectConfig.debugConfig.DUMP_ALL_QUERIES: + try: + allQueriesFile = \ + os.path.join(self.projectConfig.locationTempDir, + config.TEMP_PATH_QUERY_ALL) + allQueriesFileHandler = open(allQueriesFile, "a") + except EnvironmentError as e: + errMsg = "Error writing the candidate SMT query: %s" % e + raise GameTimeError(errMsg) + else: + with allQueriesFileHandler: + allQueriesFileHandler.write("*** CANDIDATE QUERY ***\n") + allQueriesFileHandler.write("%s\n\n" % smtQuery) - def estimate_edge_weights(self): - """ - Estimates the weights on the edges of the DAG, using the values + logger.info("Removing temporary path information files...") + self._removeTempPathFiles() + logger.info("Temporary path information files removed.") + logger.info("") + + return Path(ilpProblem, pathNodes, lineNumbers, + conditions, conditionEdges, conditionTruths, + arrayAccesses, aggIndexExprs, + smtQuery, assignments) + + def estimateEdgeWeights(self): + """Estimates the weights on the edges of the DAG, using the values of the basis "Path" objects. The result is stored in the instance variable "edgeWeights". - + Precondition: The basis paths have been generated and have values. """ - self.dag.reset_edge_weights() + self.dag.resetEdgeWeights() - basis_values = [basis_path.measured_value for basis_path - in self.basis_paths] + basisValues = [basisPath.measuredValue for basisPath + in self.basisPaths] # By default, we assume a value of 0 for each of the rows in # the basis matrix that no replacement could be found for # (the `bad' rows in the basis matrix). - basis_values += [0] * (self.path_dimension - len(basis_values)) + basisValues += [0] * (self.pathDimension - len(basisValues)) # Estimate the weights on the `non-special' edges of the graph. logger.info("Estimating the weights on the `non-special' edges...") - reduced_edge_weights = dot(inv(self.basis_matrix), basis_values) + reducedEdgeWeights = dot(inv(self.basisMatrix), basisValues) logger.info("Weights estimated.") # Generate the list of edge weights that the integer linear # programming problem will use. - logger.info("Generating the list of weights on all_temp_files edges...") - for reduced_edge_index, reduced_edge in enumerate(self.dag.edges_reduced): - self.dag.edge_weights[self.dag.edges_reduced_indices[reduced_edge]] = \ - reduced_edge_weights[reduced_edge_index] + logger.info("Generating the list of weights on all edges...") + for reducedEdgeIndex, reducedEdge in enumerate(self.dag.edgesReduced): + self.dag.edgeWeights[self.dag.edgesReducedIndices[reducedEdge]] = \ + reducedEdgeWeights[reducedEdgeIndex] logger.info("List generated.") - def generate_paths(self, *args, **kwargs): - return PathGenerator.generate_paths(self, *args, **kwargs) - - ### MEASUREMENT FUNCTIONS #### - def measure_basis_paths(self): - """Measure all generated BASIS_PATHS again + def _removeTempPathFiles(self): + """Removes the temporary path information files that are + generated when the feasibility of a path is determined. + """ + nodesFile = os.path.join(self.projectConfig.locationTempDir, + config.TEMP_PATH_NODES) + removeFile(nodesFile) + + lineNumbersFile = os.path.join(self.projectConfig.locationTempDir, + config.TEMP_PATH_LINE_NUMBERS) + removeFile(lineNumbersFile) + + conditionsFile = os.path.join(self.projectConfig.locationTempDir, + config.TEMP_PATH_CONDITIONS) + removeFile(conditionsFile) + + conditionEdgesFile = os.path.join(self.projectConfig.locationTempDir, + config.TEMP_PATH_CONDITION_EDGES) + removeFile(conditionEdgesFile) + + conditionTruthsFile = os.path.join(self.projectConfig.locationTempDir, + config.TEMP_PATH_CONDITION_TRUTHS) + removeFile(conditionTruthsFile) + + arrayAccessesFile = os.path.join(self.projectConfig.locationTempDir, + config.TEMP_PATH_ARRAY_ACCESSES) + removeFile(arrayAccessesFile) + + aggIndexExprsFile = os.path.join(self.projectConfig.locationTempDir, + config.TEMP_PATH_AGG_INDEX_EXPRS) + removeFile(aggIndexExprsFile) + + smtQueryFile = os.path.join(self.projectConfig.locationTempDir, + "%s.smt" % config.TEMP_PATH_QUERY) + removeFile(smtQueryFile) + + ### PATH VALUE FUNCTIONS ### + def writeBasisValuesToFile(self, location, measured=False): + """Convenience wrapper around the "writePathValuesToFile" method + that writes the values of the "Path" objects that represent + the feasible basis paths of the code being analyzed to a file. + + Arguments: + location: + Location of the file. + measured: + `True` if, and only if, the values that will be written to + the file are the measured values of the feasible basis paths. """ - for i in range(len(self.basis_paths)): - p: Path = self.basis_paths[i] - self.measure_path(p, f"basis_path{i}") + Analyzer.writePathValuesToFile(self.basisPaths, location, measured) - def measure_path(self, path: Path, output_name: str) -> int: + def writeTemplateBasisValuesFile(self, location): + """Creates a template file, at the location provided, which can + be used as input to the "loadBasisValuesFromFile" method. + + The template file contains instructions on how to specify + the measured values to be associated with the feasible basis + "Path" objects, and follows the grammar described in + the documentation of the "loadBasisValuesFromFile" method. + + @param location Location of the file. + """ + try: + templateBasisValuesFileHander = open(location, "w") + except EnvironmentError as e: + errMsg = ("Error writing the template file to load values " + "for the basis Path objects: %s") % e + raise GameTimeError(errMsg) + else: + with templateBasisValuesFileHander: + projectConfig = self.projectConfig + templateHeader = \ +"""# This template was generated by GameTime during the analysis of +# the function %s in the file located at +# %s. +# Below, supply the values to be associated with the Path objects +# that represent the basis paths. +""" % (projectConfig.func, projectConfig.locationOrigFile) + + contents = [] + contents.append(templateHeader) + for position in xrange(len(self.basisPaths)): + contents.append("# Append the value for basis path %d " + "to the line below." % (position+1)) + contents.append("%d\t" % (position + 1)) + contents.append("") + templateBasisValuesFileHander.write("\n".join(contents)) + + def loadBasisValuesFromFile(self, location): + """Loads the measured values of the "Path" objects that represent + the feasible basis paths of the code being analyzed from a file. + + Each line of the file should have a pair of numbers separated by + whitespace: the former is the (one-based) number of a basis + "Path" object, which is also its (one-based) position in the list + of basis "Path" objects maintained by this "Analyzer" object, while + the latter is the value to be associated with the "Path" object. + + Lines that start with a "#" character are assumed to be comments, + and are thus ignored. For a template file, refer to the + "writeTemplateBasisValuesFile" method. + + Precondition: The basis paths have been generated. + + @param location Location of the file. + """ + try: + basisValuesFileHandler = open(location, "r") + except EnvironmentError as e: + errMsg = "Error loading the values of the basis paths: %s" % e + raise GameTimeError(errMsg) + else: + with basisValuesFileHandler: + basisValuesLines = basisValuesFileHandler.readlines() + basisValuesLines = [line.strip() for line in basisValuesLines] + basisValuesLines = [line for line in basisValuesLines + if line != "" and not line.startswith("#")] + basisValuesLines = [line.split() for line in basisValuesLines] + self.loadBasisValues([(int(position), int(value)) + for position, value in basisValuesLines]) + + def loadBasisValues(self, basisValues): + """Loads the measured values of the "Path" objects that represent + the feasible basis paths of the code being analyzed from the list of + tuples provided. Each tuple has two elements: the first element is + the (one-based) position of a basis "Path" object in the list of + basis "Path" objects maintained by this "Analyzer" object, and + the second element is the measured value to be associated with + the "Path" object. + + Precondition: The basis paths have been generated. + + @param basisValues List of tuples, as described. """ - Measure the Path if never measured before. If no name was set, the parameter output_name is used. + numBasisPaths, numBasisValues = len(self.basisPaths), len(basisValues) + if numBasisPaths != numBasisValues: + errMsg = ("There are %d basis paths, but %d values " + "were provided.") % (numBasisPaths, numBasisValues) + raise GameTimeError(errMsg) + + for position, value in basisValues: + self.basisPaths[position-1].setMeasuredValue(value) + + @staticmethod + def writePathValuesToFile(paths, location, measured=False): + """Writes the values of each of the :class:`~gametime.path.Path` + objects in the list provided to a file. + + Each line of the file is a pair of numbers separated by whitespace: + the former is the (one-based) number of + a :class:`~gametime.path.Path` object, which is also its (one-based) + position in the list provided, while the latter is a value of + the :class:`~gametime.path.Path` object. + + Arguments: + paths: + List of :class:`~gametime.path.Path` objects whose values + are to be written to a file. + location: + Location of the file. + measured: + `True` if, and only if, the values that will be written to + the file are the measured values of the feasible paths. + """ + try: + pathValuesFileHandler = open(location, "w") + except EnvironmentError as e: + errMsg = "Error writing the values of the paths: %s" % e + raise GameTimeError(errMsg) + else: + with pathValuesFileHandler: + for position, path in enumerate(paths): + pathValue = (path.measuredValue if measured else + path.predictedValue) + pathValuesFileHandler.write("%d\t%d\n" % + (position+1, pathValue)) + + @staticmethod + def writeValueToFile(value, location): + """Write the given `value` into file `location`. `value` is a floating + point. It is written with 2 decimal points. For compatibility + purposes, if the `value` is an int, it is written without any decimal + points + """ + try: + valuesFileHandler = open(location, "w") + except EnvironmentError as e: + errMsg = "Error writing the value: %s" % e + raise GameTimeError(errMsg) + else: + with valuesFileHandler: + if (int(value) == value): + valuesFileHandler.write("%d\n" % value) + else: + valuesFileHandler.write("%.2f\n" % value) - Parameters: - path: Path : - The path object - output_name: str : - Name for this path. - Returns: - Measured cycle count for PATH. + + ### SERIALIZATION FUNCTIONS ### + def saveToFile(self, location): + """Saves the current state of this Analyzer object to a file. + + @param location Location of the file to save the current state + of this Analyzer object to. """ + try: + logger.info("Saving the Analyzer object to a file...") + analyzerFileHandler = bz2.BZ2File(location, "w") + except EnvironmentError as e: + errMsg = "Error saving the Analyzer object to a file: %s" % e + raise GameTimeError(errMsg) + else: + with analyzerFileHandler: + pickle.dump(self, analyzerFileHandler) + logger.info("Analyzer object saved.") - if path.path_analyzer == None or path.name != output_name: - path.name = output_name - path_analyzer: PathAnalyzer = PathAnalyzer(self.preprocessed_path, self.project_config, self.dag, path, output_name) - path.path_analyzer = path_analyzer - - path_analyzer = path.path_analyzer - value: int = path.measured_value - value = max(value, path_analyzer.measure_path(self.backend)) - path.set_measured_value(value) - return path.measured_value - - def measure_paths(self, paths: list[Path], output_name_prefix: str) -> int: + @staticmethod + def loadFromFile(location): + """Loads an Analyzer object from the file whose location is provided. + + @param location Location of the file. + @return Analyzer object, loaded from the file whose location + is provided. """ - Measure the list of PATHS. Using prefix and index as name if none is given. - - Parameters: - paths: list[Path] : - List of paths to measure. - output_name_prefix: str : - Prefix to use for the name of each path - - Returns: - List of measured values for the paths. + try: + logger.info("Loading an Analyzer object from a file...") + analyzerFileHandler = bz2.BZ2File(location, "r") + except EnvironmentError as e: + errMsg = "Error loading an Analyzer object: %s" % e + raise GameTimeError(errMsg) + else: + with analyzerFileHandler: + analyzer = pickle.load(analyzerFileHandler) + logger.info("Analyzer object loaded.") + return analyzer + + def writePathsToFiles(self, paths, writePerPath=False, rootDir=None): + """Utility method that writes information available within the Path + objects in the list provided to different files. + + All of the files are stored within directories. The hierarchy of these + directories and files is determined by the "writePerPath" argument. + + If the "writePerPath" argument is True, each directory corresponds + to one Path object. The contents of each directory are files, one + for each type of information available within the Path object. + For example, the conditions of the first Path object in the list will + be written in the file "[config.TEMP_PATH_CONDITIONS]", located in + the directory "[config.TEMP_CASE]-1". "config" is the Configuration + object of this analysis. + + If the "writePerPath" argument is False, which is the default value, + this hierarchy is `rotated'. Each directory instead corresponds to + one type of information. The contents of each directory are files, + one for each Path object. For example, the conditions of the first + Path object in the list will be written in the file + "[config.TEMP_PATH_CONDITIONS]-1", located in the directory + "[config.TEMP_PATH_CONDITIONS]". + + The "rootDir" argument specifies where the directories should be + created; if None is provided, which is the default value, they are + created in the temporary directory created by GameTime for + this analysis. Directories from a previous execution will be + overwritten. + + @param paths List of Path objects to write to files. + @param writePerPath Boolean flag, as described. True if each directory + created corresponds to one Path object; False if each directory + created corresponds to one type of information available within + a Path object. + @param rootDir Location of a root directory, as described. """ - result = [] - for i in range(len(paths)): - output_name: str = f'{output_name_prefix}{i}' - result.append(self.measure_path(paths[i], output_name)) - return result + rootDir = rootDir or self.projectConfig.locationTempDir + + def generateLocation(infoType): + """Helper function that returns the location of the file where + the provided type of information (about a Path object) will be + written. + + @param infoType Type of information (about a Path object) that + will be written, provided as a string. + @retval Location of the file where the information will be written. + """ + infoDir = os.path.join(rootDir, + ("%s-%s" % (config.TEMP_CASE, pathNum + 1)) + if writePerPath else infoType) + createDir(infoDir) + infoFile = os.path.join(infoDir, + "%s%s" % (infoType, + ("" if writePerPath + else ("-%s" % (pathNum + 1))))) + return infoFile + + for pathNum, path in enumerate(paths): + ilpProblemFile = generateLocation(config.TEMP_PATH_ILP_PROBLEM) + path.writeIlpProblemToLpFile(ilpProblemFile) + + nodeFile = generateLocation(config.TEMP_PATH_NODES) + path.writeNodesToFile(nodeFile) + + lineNumbersFile = generateLocation(config.TEMP_PATH_LINE_NUMBERS) + path.writeLineNumbersToFile(lineNumbersFile) + + conditionsFile = generateLocation(config.TEMP_PATH_CONDITIONS) + path.writeConditionsToFile(conditionsFile) + + conditionEdgesFile = \ + generateLocation(config.TEMP_PATH_CONDITION_EDGES) + path.writeConditionEdgesToFile(conditionEdgesFile) + + conditionTruthsFile = \ + generateLocation(config.TEMP_PATH_CONDITION_TRUTHS) + path.writeConditionTruthsToFile(conditionTruthsFile) + + arrayAccessesFile = \ + generateLocation(config.TEMP_PATH_ARRAY_ACCESSES) + path.writeArrayAccessesToFile(arrayAccessesFile) + + aggIndexExprsFile = \ + generateLocation(config.TEMP_PATH_AGG_INDEX_EXPRS) + path.writeAggIndexExprsToFile(aggIndexExprsFile) + + smtQueryFile = "%s.smt" % generateLocation(config.TEMP_PATH_QUERY) + path.smtQuery.writeSmtQueryToFile(smtQueryFile) + + smtModelFile = generateLocation(config.TEMP_SMT_MODEL) + path.smtQuery.writeModelToFile(smtModelFile) + + caseFile = generateLocation(config.TEMP_CASE) + path.writeAssignmentsToFile(caseFile) + + predictedValueFile = \ + generateLocation(config.TEMP_PATH_PREDICTED_VALUE) + path.writePredictedValueToFile(predictedValueFile) + measuredValueFile = \ + generateLocation(config.TEMP_PATH_MEASURED_VALUE) + path.writeMeasuredValueToFile(measuredValueFile) diff --git a/src/cilHelper.py b/src/cilHelper.py new file mode 100644 index 00000000..0f40b7df --- /dev/null +++ b/src/cilHelper.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python + +"""Exposes miscellaneous functions to interface with the CIL tool.""" + +"""See the LICENSE file, located in the root directory of +the source distribution and +at http://verifun.eecs.berkeley.edu/gametime/about/LICENSE, +for details on the GameTime license and authors. +""" + + +import os +import subprocess + +from defaults import config, sourceDir +from fileHelper import removeFiles + + +def _generateCilCommand(projectConfig, keepLineNumbers): + """Generates the system call to run CIL on the file currently + being analyzed. + + Arguments: + projectConfig: + :class:`~gametime.projectConfiguration.ProjectConfiguration` + object that represents the configuration of a GameTime project. + keepLineNumbers: + `True` if, and only if, the resulting file should contain + preprocessor directives that maintain the line numbers from + the original source file (and other included source files). + + Returns: + Appropriate system call as a list that contains the program + to be run and the proper arguments. + """ + # Set the environment variable that allows the Cilly driver to find + # the path to the configuration file for the Findlib OCaml module. + os.environ["OCAMLFIND_CONF"] = os.path.join(sourceDir, + "ocaml/conf/findlib.conf") + + # Set the environment variable that allows the Cilly driver to find + # the path to the folder that contains the compiled OCaml files. + os.environ["OCAMLPATH"] = os.path.join(sourceDir, "ocaml/lib") + + command = [] + + command.append(os.path.join(config.TOOL_CIL, "bin/cilly.bat")) + + command.append("--dooneRet") + command.append("--domakeCFG") + command.append("--dosimpleMem") + command.append("--disallowDuplication") + if not keepLineNumbers: + command.append("--noPrintLn") + + command.append("--dopartial") + command.append("--partial_root_function=%s" % projectConfig.func) + + command.append(projectConfig.locationTempFile) + + command.append("-I'%s'" % projectConfig.locationOrigDir) + for includePath in projectConfig.included: + command.append("-I'%s'" % includePath) + + command.append("--save-temps='%s'" % projectConfig.locationTempDir) + command.append("-c") + command.append("-o") + command.append("'%s.out'" % projectConfig.locationTempNoExtension) + + return command + +def runCil(projectConfig, keepLineNumbers=False): + """Conducts the sequence of system calls that will run CIL on the + file currently being analyzed. + + Arguments: + projectConfig: + :class:`~gametime.projectConfiguration.ProjectConfiguration` + object that represents the configuration of a GameTime project. + keepLineNumbers: + `True` if, and only if, the resulting file should contain + preprocessor directives that maintain the line numbers from + the original source file (and other included source files). + + Returns: + Zero if the calls were successful; a non-zero value otherwise. + """ + command = _generateCilCommand(projectConfig, keepLineNumbers) + print " ".join(command) + return subprocess.call(command, shell=True) + +def removeTempCilFiles(projectConfig): + """Removes the temporary files created by CIL during its analysis. + + Arguments: + projectConfig: + :class:`~gametime.projectConfiguration.ProjectConfiguration` + object that represents the configuration of a GameTime project. + """ + # Remove the files with extension ".cil.*". + otherTempFiles = r".*\.cil\..*" + removeFiles([otherTempFiles], projectConfig.locationTempDir) + + # By this point, we have files that are named the same as the + # temporary file for GameTime, but that have different extensions. + # Remove these files. + otherTempFiles = r".*-gt\.[^c]+" + removeFiles([otherTempFiles], projectConfig.locationTempDir) diff --git a/src/clang_helper.py b/src/clang_helper.py deleted file mode 100644 index f4dbfc47..00000000 --- a/src/clang_helper.py +++ /dev/null @@ -1,290 +0,0 @@ -#!/usr/bin/env python - -""" Functions to help interacting with clang on the command -line. Allows creation of dags -""" -import os -import subprocess -from typing import List - -from defaults import logger -from file_helper import remove_files -from project_configuration import ProjectConfiguration -import command_utils - - -def compile_to_llvm_for_exec(c_filepath: str, output_file_folder: str, output_name: str, extra_libs: List[str]=[], extra_flags: List[str]=[], readable: bool = False) -> str: - """ - Compile the C program into bitcode in OUTPUT_FILE_FOLDER. - - Parameters: - c_filepath: str : - Path to the input C program. Main function should be defined. - output_file_folder: str : - Storage folder for generated file. - output_name: str : - Name for generated bc. - extra_libs: List[str] : - Extra libraries needed for compilation. (Default value = []) - extra_flags: List[str] : - Extra flags needed for compilation. (Default value = []) - readable: bool : - If set to true, also generate readable LL file. (Default value = False) - Returns: - str: - The path to bc. - """ - # compile bc file - file_to_compile: str = c_filepath - output_file: str = os.path.join(output_file_folder, f"{output_name}.bc") - - commands: List[str] = ["clang", "-emit-llvm", "-O0", "-o", output_file, "-c", file_to_compile] + extra_flags - for lib in extra_libs: - commands.append(f"-I{lib}") - command_utils.run(commands) - - if readable: - # translate for .ll automatically. - ll_output_file: str = os.path.join(output_file_folder, f"{output_name}.ll") - commands = ["llvm-dis", output_file, "-o", ll_output_file] - command_utils.run(commands) - - return output_file - -def compile_list_to_llvm_for_analysis(c_filepaths: List[str] , output_file_folder: str, extra_libs: List[str]=[], extra_flags: List[str]=[], readable: bool = True) -> List[str]: - compiled_files = [] - for c_filepath in c_filepaths: - compiled_files.append(compile_to_llvm_for_analysis(c_filepath, output_file_folder, f"{c_filepath[:-2]}gt", extra_libs, extra_flags, readable)) - return compiled_files - -def compile_to_llvm_for_analysis(c_filepath: str , output_file_folder: str, output_name: str, extra_libs: List[str]=[], extra_flags: List[str]=[], readable: bool = True) -> str: - """ - Compile the C program into bitcode in OUTPUT_FILE_FOLDER using -O0 option to preserve maximum structure. - - Parameters: - c_filepath: str : - Path to the input C program. - output_file_folder: str : - Storage folder for generated file. - output_name: str : - Name for generated bc. - extra_libs: List[str] : - Extra libraries needed for compilation. (Default value = []) - extra_flags: List[str] : - Extra flags needed for compilation. (Default value = []) - readable: bool : - If set to true, also generate readable LL file. (Default value = False) - Returns: - str: - The path to bc. - """ - # compile bc file - file_to_compile: str = c_filepath - output_file: str = os.path.join(output_file_folder, f"{output_name}.bc") - - # "-Wno-implicit-function-declaration" is required so that clang - # does not report "undeclared function '__assert_fail'" - commands: List[str] = ["clang", "-emit-llvm", "-Xclang","-disable-O0-optnone", "-Wno-implicit-function-declaration", "-c", file_to_compile, "-o", output_file] + extra_flags - for lib in extra_libs: - commands.append(f"-I{lib}") - command_utils.run(commands) - - if readable: - # translate for .ll automatically. (optional) - ll_output_file: str = os.path.join(output_file_folder, f"{output_name}.ll") - commands = ["llvm-dis", output_file, "-o", ll_output_file] - command_utils.run(commands) - return output_file - -def bc_to_executable(bc_filepath: str, output_folder: str, output_name: str, extra_libs: List[str]=[], extra_flags: List[str]=[]) -> str: - """ - Compile the LLVM bitcode program into executable in OUTPUT_FILE_FOLDER. - - Parameters: - bc_filepath: str : - Path to the input bitcode program. - output_folder: str : - Storage folder for generated file. - output_name: str : - Name for generated executable. - extra_libs: List[str] : - Extra libraries needed for compilation. (Default value = []) - extra_flags: List[str] : - Extra flags needed for compilation. (Default value = []) - Returns: - str: - The path to executable. - """ - # Set the path for the output executable file - executable_file = os.path.join(output_folder, output_name) - - # Prepare the clang command - clang_commands = ["clang", bc_filepath, "-o", executable_file] + extra_flags - - # Add extra include directories or libraries - for lib in extra_libs: - clang_commands.extend(["-I", lib]) - - # Run clang to compile the bitcode into an executable - command_utils.run(clang_commands) - - return executable_file - - -def dump_object(object_filepath: str, output_folder: str, output_name: str) -> str: - """ - Dump the .o file to OUTPUT_NAME.dmp - - Parameters: - object_filepath: str : - The name of the .o file to dump - output_folder: str : - The folder path where .dmp files will be stored - output_name: str : - Name for dumped .dmp files. - - Returns: - str: - Path of the output OUTPUT_NAME.dmp file - - """ - - output_file: str = os.path.join(output_folder, f"{output_name}.dmp") - - commands: List[str] = ["riscv32-unknown-elf-objdump", "--target=riscv32", "-march=rv32i", object_filepath, "-c", "-o", output_file] - command_utils.run(commands) - return output_file - -def generate_dot_file(bc_filename: str, bc_file_folder: str, output_name: str = "main") -> str: - """ - Create dag from .bc file using opt through executing shell commands - - Parameters: - bc_filename: str : - location of the compiled llvm .bc file - bc_file_folder: str : - the folder path where .bc files is stored and where .main.dot file will be stored - output_name: str : - Name of the generated dot file (Default value = "main") - - Returns: - str: - Path of the output .dot file - - """ - output_file: str = f".{output_name}.dot" - cur_cwd: str = os.getcwd() - os.chdir(bc_file_folder) # opt generates .dot in cwd - commands: List[str] = ["opt", "-passes=dot-cfg", "-S", "-disable-output", bc_filename] - command_utils.run(commands) - os.chdir(cur_cwd) - return output_file - - -def inline_functions(bc_filepath: str, output_file_folder: str, output_name: str) -> str: - """ - Unrolls the provided input file and output the unrolled version in - the output file using llvm's opt utility. Could be unreliable if input_file - is not compiled with `compile_to_llvm_for_analysis` function. If that is the case, the - user might want to generate their own unrolled .bc/.ll file rather than - relying on this built-in function. - - Parameters: - bc_filepath: str : - Input .bc/.ll function to loop unroll - output_file_folder: str : - folder to write unrolled .bc file. Outputs in a - human-readable form already. - output_name: str : - file to write unrolled .bc file. Outputs in a - human-readable form already. - - Returns: - str: - Path of the output .bc file - - """ - output_file: str = os.path.join(output_file_folder, f"{output_name}.bc") - - commands: List[str] = ["opt", - "-passes=\"always-inline,inline\"" - "-inline-threshold=10000000", - "-S", bc_filepath, - "-o", output_file] - - command_utils.run(commands) - return output_file - - -def unroll_loops(bc_filepath: str, output_file_folder: str, output_name: str, project_config: ProjectConfiguration) -> str: - """ - Unrolls the provided input file and output the unrolled version in - the output file using llvm's opt utility. Could be unreliable if input_file - is not compiled with `compile_to_llvm_for_analysis` function. If that is the case, the - user might want to generate their own unrolled .bc/.ll file rather than - relying on this built-in function. - - Parameters: - input_file: str : - Input .bc/.ll function to loop unroll - output_file_folder: str : - folder to write unrolled .bc file. Outputs in a - human-readable form already. - output_name: str : - file to write unrolled .bc file. Outputs in a - human-readable form already. - project_config: ProjectConfiguration : - ProjectConfiguration this helper is calling from. - - Returns: - str: - Path of the output .bc file - - """ - # return bc_filepath - output_file: str = os.path.join(output_file_folder, f"{output_name}.bc") - - # Related but unused passes: - # -unroll-threshold=10000000, -unroll-count=4, - # -unroll-allow-partial, -instcombine, - # -reassociate, -indvars, -mem2reg - commands: List[str] = ["opt", - "-passes='simplifycfg,loops,lcssa,loop-simplify,loop-rotate,indvars,loop-unroll'" - "-S", bc_filepath, - "-o", output_file] - - command_utils.run(commands) - - return output_file - -def remove_temp_cil_files(project_config: ProjectConfiguration, all_temp_files=False) -> None: - """ - Removes the temporary files created by CIL during its analysis. - - Parameters: - project_config : - ProjectConfiguration this helper is calling from. - - all_temp_files: - True if all files in temperary directory should be removed. - """ - # Remove the files with extension ".cil.*". - if all_temp_files: - remove_files([r".*"], project_config.location_temp_dir) - return - - other_temp_files = r".*\.dot" - remove_files([other_temp_files], project_config.location_temp_dir) - - other_temp_files = r".*\.bc" - remove_files([other_temp_files], project_config.location_temp_dir) - - other_temp_files = r".*\.ll" - remove_files([other_temp_files], project_config.location_temp_dir) - - # By this point, we have files that are named the same as the - # temporary file for GameTime, but that have different extensions. - # Remove these files. - other_temp_files = r".*-gt\.[^c]+" - remove_files([other_temp_files], project_config.location_temp_dir) - diff --git a/src/command_utils.py b/src/command_utils.py deleted file mode 100644 index 5851b25e..00000000 --- a/src/command_utils.py +++ /dev/null @@ -1,12 +0,0 @@ -import subprocess -import sys - -def run(command, shell=False): - print(f"==> Executing command: {' '.join(command)}") - result = subprocess.run(command, shell=shell, check=True) - if result.returncode != 0: - print(f"Error running command: {command}") - print(result.stdout) - print(result.stderr) - sys.exit(1) - return result.stdout \ No newline at end of file diff --git a/src/config.xml.in b/src/config.xml.in new file mode 100644 index 00000000..69a1e5e4 --- /dev/null +++ b/src/config.xml.in @@ -0,0 +1,226 @@ + + + + + + + + http://verifun.eecs.berkeley.edu/gametime + + @VERSION@ + + + http://verifun.eecs.berkeley.edu/gametime/static/php/version.php?latest + + + + + + + 32 + + little + + + + + + __gt_assume + + __gt_simulate + + + + + + __gtAGG_ + + __gtCONSTRAINT + + __gtEFC_ + + __gtFIELD_ + + __gtINDEX + + __gtPTR + + __gt + + + + + + project-config + + merged + + loop-config + + + -gt + + + -merged + + -unrolled + + -inlined + + -line-nums + + + create-dag + + dag + + dag-id-map + + + ir + + find-conditions + + + path-ilp-problem + + path-nodes + + path-line-numbers + + path-conditions + + path-condition-edges + + path-condition-truths + + path-array-accesses + + path-agg-index-exprs + + path-value-predicted + + path-value-measured + + path-all + + + path-query + + smt-model + + path-query-all + + + case + + + basis-matrix + + measurement + + basis-values + + dag-weights + + distribution + + + + + + bin/GameTime.dll + + cil + + + + + + @BOOLECTOR_EXE@ + + @Z3_PYC@ + + + + + + @GNU_ARM@ + + @PTARM@ + + diff --git a/src/config.yaml.in b/src/config.yaml.in deleted file mode 100644 index d8d6dbb2..00000000 --- a/src/config.yaml.in +++ /dev/null @@ -1,58 +0,0 @@ -!!python/object:gametime_configuration.GametimeConfiguration - -## gametime -# TODO: Point to new URL -WEBSITE_URL: http://verifun.eecs.berkeley.edu/gametime -VERSION: 1.5 # TODO: Find way to not hardcode this value -LATEST_VERSION_INFO_URL: http://verifun.eecs.berkeley.edu/gametime/static/php/version.php?latest - -## memory -WORD_BITSIZE: 32 -ENDIANESS: little - -## Annotations -ANNOTATION_ASSUME: __gt_assume -ANNOTATION_SIMULATE: __gt_simulate - -## Identifiers -IDENT_AGGREGATE: __gtAGG_ -IDENT_CONSTRAINT: __gtCONSTRAINT -IDENT_EFC: __gtEFC -IDENT_FILED: __gtFIELD -IDENT_TEMPINDEX: __gtINDEX -IDENT_TEMPPTR: __gtPTR -IDENT_TEMPVAR: __gt - -## temps -TEMP_PROJECT_CONFIG: project-config -TEMP_MERGED: merged -TEMP_LOOP_CONFIG: loop-config -TEMP_SUFFIX: gt -TEMP_SUFFIX_MERGED: merged -TEMP_SUFFIX_UNROLLED: unrolled -TEMP_SUFFIX_INLINED: inlined -TEMP_SUFFIX_LINE_NUMS: -line-nums -TEMP_PHX_CREATE_DAG: create-dag -TEMP_DAG: .main.dot -TEMP_DAG_ID_MAP: dag-id-map -TEMP_PHX_IR: ir -TEMP_PHX_FIND_CONDITIONS: find-conditions -TEMP_PATH_ILP_PROBLEM: path-ilp-problem -TEMP_PATH_NODES: path-nodes -TEMP_PATH_CONDITIONS: path-line-numbers -TEMP_PATH_CONDITION_EDGES: path-conditions -TEMP_PATH_CONDITION_TRUTHS: path-condition-edges -TEMP_PATH_LINE_NUMBERS: path-condition-truths -TEMP_PATH_ARRAY_ACCESSES: path-array-accesses -TEMP_PATH_AGG_INDEX_EXPRS: path-agg-index-exprs -TEMP_PATH_PREDICTED_VALUE: path-value-predicted -TEMP_PATH_MEASURED_VALUE: path-value-measured -TEMP_PATH_ALL: path-all -TEMP_PATH_QUERY: path-query -TEMP_PATH_QUERY_ALL: path-query-all -TEMP_CASE: case -TEMP_BASIS_MATRIX: basis-matrix -TEMP_MEASUREMENT: measurement -TEMP_BASIS_VALUES: basis-values -TEMP_DAG_WEIGHTS: dag-weights -TEMP_DISTRIBUTION: distribution \ No newline at end of file diff --git a/src/configuration.py b/src/configuration.py new file mode 100644 index 00000000..89d03a97 --- /dev/null +++ b/src/configuration.py @@ -0,0 +1,493 @@ +#!/usr/bin/env python + +"""Exposes classes and functions to maintain information +necessary to configure GameTime. +""" + +"""See the LICENSE file, located in the root directory of +the source distribution and +at http://verifun.eecs.berkeley.edu/gametime/about/LICENSE, +for details on the GameTime license and authors. +""" + + +import imp +import os +import sys + +from xml.dom import minidom + +from defaults import logger +from gametimeError import GameTimeError + + +class Endianness(object): + """This class represents the endianness of the target machine.""" + # Big-endian. + BIG = 0 + # Little-endian. + LITTLE = 1 + + +class Configuration(object): + """Stores information necessary to configure GameTime. + """ + def __init__(self): + """Constructor for the Configuration class.""" + ### GAMETIME INFORMATION ### + # URL of the website for GameTime. + self.WEBSITE_URL = "" + + # Current version number of GameTime. + self.VERSION = "" + + # URL that provides information about + # the latest version of GameTime. + self.LATEST_VERSION_INFO_URL = "" + + ### FILE INFORMATION ### + # Full location of the configuration file. + self.configFile = "" + + # Directory that contains the configuration file. + self.configDir = "" + + ### MEMORY LAYOUT INFORMATION ### + # Word size on the machine that GameTime is being run on (in bits). + # This value should be changed if GameTime will be run on a + # non-32-bit machine. + self.WORD_BITSIZE = 32 + + # Word size on the machine that GameTime is being run on (in bytes). + # This value should be changed and the solution should be recompiled, + # if GameTime will be run on a non-32-bit machine. + self.WORD_BYTESIZE = 4 + + # Endianness of the target machine. + self.ENDIANNESS = Endianness.LITTLE + + ### ANNOTATIONS ### + # Annotation that is used when additional conditions need to be + # provided to GameTime. + self.ANNOTATION_ASSUME = "" + + # Annotation that is used when a simulation is performed. + self.ANNOTATION_SIMULATE = "" + + ### SPECIAL IDENTIFIERS ### + # The special identifiers and for the names and prefixes of temporary + # files and folders are described in the default GameTime + # configuration XML file provided in the source directory. + self.IDENT_AGGREGATE = "" + self.IDENT_CONSTRAINT = "" + self.IDENT_EFC = "" + self.IDENT_FIELD = "" + self.IDENT_TEMPINDEX = "" + self.IDENT_TEMPPTR = "" + self.IDENT_TEMPVAR = "" + + self.TEMP_PROJECT_CONFIG = "" + self.TEMP_MERGED = "" + self.TEMP_LOOP_CONFIG = "" + + self.TEMP_SUFFIX = "" + + self.TEMP_SUFFIX_MERGED = "" + self.TEMP_SUFFIX_UNROLLED = "" + self.TEMP_SUFFIX_INLINED = "" + self.TEMP_SUFFIX_LINE_NUMS = "" + + self.TEMP_PHX_CREATE_DAG = "" + self.TEMP_DAG = "" + self.TEMP_DAG_ID_MAP = "" + + self.TEMP_PHX_IR = "" + self.TEMP_PHX_FIND_CONDITIONS = "" + + self.TEMP_PATH_ILP_PROBLEM = "" + self.TEMP_PATH_NODES = "" + self.TEMP_PATH_CONDITIONS = "" + self.TEMP_PATH_CONDITION_EDGES = "" + self.TEMP_PATH_CONDITION_TRUTHS = "" + self.TEMP_PATH_LINE_NUMBERS = "" + self.TEMP_PATH_ARRAY_ACCESSES = "" + self.TEMP_PATH_AGG_INDEX_EXPRS = "" + self.TEMP_PATH_PREDICTED_VALUE = "" + self.TEMP_PATH_MEASURED_VALUE = "" + self.TEMP_PATH_ALL = "" + + self.TEMP_PATH_QUERY = "" + self.TEMP_SMT_MODEL = "" + self.TEMP_PATH_QUERY_ALL = "" + + self.TEMP_CASE = "" + + self.TEMP_BASIS_MATRIX = "" + self.TEMP_MEASUREMENT = "" + self.TEMP_BASIS_VALUES = "" + self.TEMP_DAG_WEIGHTS = "" + self.TEMP_DISTRIBUTION = "" + + ### TOOLS ### + # Absolute location of the Phoenix DLL. + self.TOOL_PHOENIX = "" + + # Absolute location of the directory that contains the CIL source code. + self.TOOL_CIL = "" + + ### SMT SOLVERS ### + # Absolute location of the Boolector executable. + self.SOLVER_BOOLECTOR = "" + + # Absolute location of the Python frontend of Z3, + # the SMT solver from Microsoft. + self.SOLVER_Z3 = "" + + ### SIMULATORS AND AUXILIARY TOOLS ### + # Absolute location of the directory that contains + # the GNU ARM toolchain. + self.SIMULATOR_TOOL_GNU_ARM = "" + + # Absolute location of the directory that contains + # the PTARM simulator. + self.SIMULATOR_PTARM = "" + +def _getText(node): + """ + Obtains the text from the node provided. + + @param node Node to obtain the text from. + @retval Text from the node provided. + """ + return " ".join(child.data.strip() for child in node.childNodes + if child.nodeType == child.TEXT_NODE) + +def _getAbsolutePath(name): + """ + Obtains the absolute path of the executable whose name is provided, + if the executable is present in any of the directories in + the PATH environment variable. + + (Based on code and suggestions at http://stackoverflow.com/q/775351) + + @param name Name of the executable. + @retval Absolute path of the executable, if present in any of + the directories in the PATH environment variable; None otherwise. + """ + if name is "": + return None + + extensions = os.environ.get("PATHEXT", "").split(os.pathsep) + pathDirs = os.environ.get("PATH", "").split(os.pathsep) + pathDirs.append(os.getcwd()) + for directory in pathDirs: + basePath = os.path.normpath(os.path.join(directory, name)) + options = [basePath] + [(basePath + ext) for ext in extensions] + for absPath in options: + if os.access(absPath, os.X_OK): + return absPath + +def readConfigFile(location): + """ + Reads GameTime configuration information from the XML file provided. + + @param location Location of the XML file that contains GameTime + configuration information. + @retval Configuration object that contains information from + the file provided. + """ + if not os.path.exists(location): + errMsg = "Cannot find configuration file: %s " % location + raise GameTimeError(errMsg) + + config = Configuration() + try: + configDom = minidom.parse(location) + except EnvironmentError as e: + errMsg = "Error reading configuration from configuration file: %s" % e + raise GameTimeError(errMsg) + + # Check that the root element is properly named. + rootNode = configDom.documentElement + if rootNode.tagName != 'gametime-config': + raise GameTimeError("The root element in the XML file should be " + "named `gametime-config`.") + + # Check that no child element of the root element has an illegal tag. + rootChildNodes = [node for node in rootNode.childNodes + if node.nodeType == node.ELEMENT_NODE] + for childNode in rootChildNodes: + childNodeTag = childNode.tagName + if childNodeTag not in ["gametime", "memory", + "annotations", "identifiers", "temps", + "tools", "smt-solvers", "simulators"]: + raise GameTimeError("Unrecognized tag: %s" % childNodeTag) + + # Get the absolute path of the file and the directory + # that contains the file. + configFileRealPath = os.path.realpath(location) + config.configFile = configFileRealPath + config.configDir = os.path.dirname(config.configFile) + + # Process the information about GameTime. + gametimeNode = configDom.getElementsByTagName("gametime")[0] + + for node in gametimeNode.childNodes: + if node.nodeType == node.ELEMENT_NODE: + nodeText = _getText(node) + nodeTag = node.tagName + + if nodeTag == "website-url": + config.WEBSITE_URL = nodeText + elif nodeTag == "version": + config.VERSION = nodeText + elif nodeTag == "latest-version-info-url": + config.LATEST_VERSION_INFO_URL = nodeText + else: + raise GameTimeError("Unrecognized tag: %s" % nodeTag) + + # Process the memory layout information. + memoryNode = configDom.getElementsByTagName("memory")[0] + + for node in memoryNode.childNodes: + if node.nodeType == node.ELEMENT_NODE: + nodeText = _getText(node) + nodeTag = node.tagName + + if nodeTag == "bitsize": + config.WORD_BITSIZE = int(nodeText) + config.WORD_BYTESIZE = config.WORD_BITSIZE / 8 + elif nodeTag == "endianness": + nodeText = nodeText.lower() + if nodeText not in ["big", "little"]: + errMsg = ("Incorrect option for the endianness " + "of the target machine: %s") % nodeText + raise GameTimeError(errMsg) + config.ENDIANNESS = Endianness.BIG if \ + nodeText == "big" else Endianness.LITTLE + else: + raise GameTimeError("Unrecognized tag: %s" % nodeTag) + + # Process the annotations that can be added to the code under analysis. + annotationsNode = configDom.getElementsByTagName("annotations")[0] + + for node in annotationsNode.childNodes: + if node.nodeType == node.ELEMENT_NODE: + nodeText = _getText(node) + nodeTag = node.tagName + + if nodeTag == "assume": + config.ANNOTATION_ASSUME = nodeText + elif nodeTag == "simulate": + config.ANNOTATION_SIMULATE = nodeText + else: + raise GameTimeError("Unrecognized tag: %s" % nodeTag) + + # Process the special identifiers. + identsNode = configDom.getElementsByTagName("identifiers")[0] + + for node in identsNode.childNodes: + if node.nodeType == node.ELEMENT_NODE: + nodeText = _getText(node) + nodeTag = node.tagName + + if nodeTag == "aggregate": + config.IDENT_AGGREGATE = nodeText + elif nodeTag == "constraint": + config.IDENT_CONSTRAINT = nodeText + elif nodeTag == "efc": + config.IDENT_EFC = nodeText + elif nodeTag == "field": + config.IDENT_FIELD = nodeText + elif nodeTag == "tempindex": + config.IDENT_TEMPINDEX = nodeText + elif nodeTag == "tempptr": + config.IDENT_TEMPPTR = nodeText + elif nodeTag == "tempvar": + config.IDENT_TEMPVAR = nodeText + else: + raise GameTimeError("Unrecognized tag: %s" % nodeTag) + + # Process the names for temporary files and folders that + # are generated during the GameTime toolflow. + tempsNode = configDom.getElementsByTagName("temps")[0] + + for node in tempsNode.childNodes: + if node.nodeType == node.ELEMENT_NODE: + nodeText = _getText(node) + nodeTag = node.tagName + + if nodeTag == "project-config": + config.TEMP_PROJECT_CONFIG = nodeText + elif nodeTag == "merged": + config.TEMP_MERGED = nodeText + elif nodeTag == "loop-config": + config.TEMP_LOOP_CONFIG = nodeText + + elif nodeTag == "suffix": + config.TEMP_SUFFIX = nodeText + + elif nodeTag == "suffix-merged": + config.TEMP_SUFFIX_MERGED = nodeText + elif nodeTag == "suffix-unrolled": + config.TEMP_SUFFIX_UNROLLED = nodeText + elif nodeTag == "suffix-inlined": + config.TEMP_SUFFIX_INLINED = nodeText + elif nodeTag == "suffix-line-nums": + config.TEMP_SUFFIX_LINE_NUMS = nodeText + + elif nodeTag == "phx-create-dag": + config.TEMP_PHX_CREATE_DAG = nodeText + elif nodeTag == "dag": + config.TEMP_DAG = nodeText + elif nodeTag == "dag-id-map": + config.TEMP_DAG_ID_MAP = nodeText + + elif nodeTag == "phx-ir": + config.TEMP_PHX_IR = nodeText + elif nodeTag == "phx-find-conditions": + config.TEMP_PHX_FIND_CONDITIONS = nodeText + + elif nodeTag == "path-ilp-problem": + config.TEMP_PATH_ILP_PROBLEM = nodeText + elif nodeTag == "path-nodes": + config.TEMP_PATH_NODES = nodeText + elif nodeTag == "path-conditions": + config.TEMP_PATH_CONDITIONS = nodeText + elif nodeTag == "path-condition-edges": + config.TEMP_PATH_CONDITION_EDGES = nodeText + elif nodeTag == "path-condition-truths": + config.TEMP_PATH_CONDITION_TRUTHS = nodeText + elif nodeTag == "path-line-numbers": + config.TEMP_PATH_LINE_NUMBERS = nodeText + elif nodeTag == "path-array-accesses": + config.TEMP_PATH_ARRAY_ACCESSES = nodeText + elif nodeTag == "path-agg-index-exprs": + config.TEMP_PATH_AGG_INDEX_EXPRS = nodeText + elif nodeTag == "path-predicted-value": + config.TEMP_PATH_PREDICTED_VALUE = nodeText + elif nodeTag == "path-measured-value": + config.TEMP_PATH_MEASURED_VALUE = nodeText + elif nodeTag == "path-all": + config.TEMP_PATH_ALL = nodeText + + elif nodeTag == "path-query": + config.TEMP_PATH_QUERY = nodeText + elif nodeTag == "smt-model": + config.TEMP_SMT_MODEL = nodeText + elif nodeTag == "path-query-all": + config.TEMP_PATH_QUERY_ALL = nodeText + + elif nodeTag == "case": + config.TEMP_CASE = nodeText + + elif nodeTag == "basis-matrix": + config.TEMP_BASIS_MATRIX = nodeText + elif nodeTag == "measurement": + config.TEMP_MEASUREMENT = nodeText + elif nodeTag == "basis-values": + config.TEMP_BASIS_VALUES = nodeText + elif nodeTag == "dag-weights": + config.TEMP_DAG_WEIGHTS = nodeText + elif nodeTag == "distribution": + config.TEMP_DISTRIBUTION = nodeText + else: + raise GameTimeError("Unrecognized tag: %s" % nodeTag) + + # Process the locations of useful tools. + toolsNode = configDom.getElementsByTagName("tools") + + for node in toolsNode[0].childNodes: + if node.nodeType == node.ELEMENT_NODE: + nodeText = _getText(node) + nodeTag = node.tagName + + configDir = config.configDir + location = os.path.normpath(os.path.join(configDir, nodeText)) + if not os.path.exists(location): + errMsg = "Invalid location: %s" % location + raise GameTimeError(errMsg) + if nodeTag == "cil": + config.TOOL_CIL = location + elif nodeTag == "phoenix": + config.TOOL_PHOENIX = location + else: + raise GameTimeError("Unrecognized tag: %s" % nodeTag) + + # Process the locations of SMT solvers. + smtSolversNode = configDom.getElementsByTagName("smt-solvers") + + for node in smtSolversNode[0].childNodes: + if node.nodeType == node.ELEMENT_NODE: + nodeText = _getText(node) + nodeTag = node.tagName + + if nodeTag == "boolector": + boolectorPath = _getAbsolutePath(nodeText) + if boolectorPath is None: + warnMsg = ("Unable to find the Boolector executable. " + "A GameTime project will not be able to " + "use Boolector as its backend SMT solver.") + logger.warn("WARNING: %s" % warnMsg) + logger.warn("") + config.SOLVER_BOOLECTOR = "" + else: + config.SOLVER_BOOLECTOR = boolectorPath + elif nodeTag == "z3": + z3Path = _getAbsolutePath(nodeText) + if z3Path is not None: + sys.path.append(os.path.dirname(z3Path)) + try: + z3Path = imp.find_module("z3")[1] + config.SOLVER_Z3 = z3Path + except ImportError: + # The Z3 Python frontend is not present in + # either the PATH environment variable or + # the PYTHONPATH environment variable. + warnMsg = ("Unable to find the Z3 Python frontend. " + "A GameTime project will not be able to " + "use Z3 as its backend SMT solver.") + logger.warn("WARNING: %s" % warnMsg) + logger.warn("") + config.SOLVER_Z3 = "" + else: + raise GameTimeError("Unrecognized tag: %s" % nodeTag) + + # Process the locations of simulators and useful auxiliary tools. + simulatorsNode = configDom.getElementsByTagName("simulators") + + for node in simulatorsNode[0].childNodes: + if node.nodeType == node.ELEMENT_NODE: + nodeText = _getText(node) + nodeTag = node.tagName + + if nodeTag == "gnu-arm": + gnuArmPath = _getAbsolutePath(nodeText) + if gnuArmPath is None: + warnMsg = ("Unable to find the location of the directory " + "that contains the GNU ARM toolchain. " + "A GameTime project will not be able to " + "use the GNU ARM toolchain to measure " + "the cycle counts of test cases.") + logger.warn("WARNING: %s" % warnMsg) + logger.warn("") + config.SIMULATOR_TOOL_GNU_ARM = "" + else: + config.SIMULATOR_TOOL_GNU_ARM = gnuArmPath + elif nodeTag == "ptarm": + ptarmPath = _getAbsolutePath(nodeText) + if ptarmPath is None: + warnMsg = ("Unable to find the location of the directory " + "that contains the PTARM simulator. " + "A GameTime project will not be able to use " + "the PTARM simulator to measure the cycle " + "counts of test cases.") + logger.warn("WARNING: %s" % warnMsg) + logger.warn("") + config.SIMULATOR_PTARM = "" + else: + config.SIMULATOR_PTARM = ptarmPath + else: + raise GameTimeError("Unrecognized tag: %s" % nodeTag) + + return config diff --git a/src/custom_inline.cpp b/src/custom_inline.cpp deleted file mode 100644 index 0060efb8..00000000 --- a/src/custom_inline.cpp +++ /dev/null @@ -1,54 +0,0 @@ -#include "llvm/IR/PassManager.h" -#include "llvm/IR/Module.h" -#include "llvm/IR/Function.h" -#include "llvm/IR/Attributes.h" -#include "llvm/Support/raw_ostream.h" -#include "llvm/Support/CommandLine.h" -#include "llvm/Passes/PassPlugin.h" -#include "llvm/Transforms/PassBuilder.h" - - -using namespace llvm; - - -static cl::opt AnalysedFunction("analysed_func", cl::init("")); - -struct CustomInlinePass : public PassInfoMixin { - - PreservedAnalyses run(Module &M, ModuleAnalysisManager &) { - bool changed = false; - - for (Function &F : M) { - if (F.getName() == AnalysedFunction) { - F.addFnAttr(Attribute::NoInline); - errs() << "Function " << F.getName() << " is not inlined.\n"; - changed = false; - } else { - F.addFnAttr(Attribute::AlwaysInline); - errs() << "Function " << F.getName() << " is inlined.\n"; - changed = true; - } - } - return changed ? PreservedAnalyses::none() : PreservedAnalyses::all(); - } -}; - -/* -extern "C" LLVM_ATTRIBUTE_WEAK PassPluginLibraryInfo llvmGetPassPluginInfo() { - return { - LLVM_PLUGIN_API_VERSION, "CustomInlinePass", "v0.1", - [](PassBuilder &PB) { - PB.registerPipelineParsingCallback( - [](StringRef Name, ModulePassManager &MPM, - ArrayRef) { - if (Name == "custom-inline") { - MPM.addPass(CustomInlinePass()); - return true; - } - return false; - } - ); - } - }; -} -*/ \ No newline at end of file diff --git a/src/defaults.py b/src/defaults.py index 12393de2..58977603 100644 --- a/src/defaults.py +++ b/src/defaults.py @@ -10,32 +10,35 @@ for details on the GameTime license and authors. """ + import logging -import logging_helper +import loggingHelper import os + # Initialize the GameTime logger (as described in # http://docs.python.org/2/howto/logging-cookbook.html). -logger: logging.Logger = logging.getLogger("gametime") -logging_helper.initialize(logger) +logger = logging.getLogger("gametime") +loggingHelper.initialize(logger) + # This import is done later, so that the module # :module:`~gametime.configuration` can use the GameTime logger. -import gametime_configuration +import configuration + #: Default directory that contains the source files of GameTime. -source_dir: str = os.path.dirname(os.path.abspath(__file__)) -#: Default configuration YAML file. -config_file: str = os.path.join(source_dir, "config.yaml.in") +sourceDir = os.path.dirname(os.path.abspath(__file__)) +#: Default configuration XML file. +configFile = os.path.join(sourceDir, "config.xml") #: Default directory that contains the GameTime GUI. -gui_dir: str = os.path.join(source_dir, "gui") +guiDir = os.path.join(sourceDir, "gui") -logger.info("Reading GameTime configuration in %s..." % config_file) +logger.info("Reading GameTime configuration in %s..." % configFile) logger.info("") -#: Default :class:`~gametime.configuration.GametimeConfiguration` object +#: Default :class:`~gametime.configuration.Configuration` object #: that represents the configuration of GameTime. -config: gametime_configuration.GametimeConfiguration = gametime_configuration.read_gametime_config_yaml(config_file) - +config = configuration.readConfigFile(configFile) logger.info("Successfully configured GameTime.") logger.info("") diff --git a/src/fileHelper.py b/src/fileHelper.py new file mode 100644 index 00000000..106cc1f9 --- /dev/null +++ b/src/fileHelper.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python + +"""Exposes miscellaneous functions to perform operations +on files and directories, such as creation, removal and movement. +""" + +"""See the LICENSE file, located in the root directory of +the source distribution and +at http://verifun.eecs.berkeley.edu/gametime/about/LICENSE, +for details on the GameTime license and authors. +""" + + +import errno +import os +import re +import shutil + +from gametimeError import GameTimeError + + +def createDir(location): + """Creates the leaf directory in the path specified, along with any + intermediate-level directories needed to contain the directory. + This is a wrapper around the :func:`~os.makedirs` function of + the :mod:`os` module, but does not raise an exception if + the directory is already present, + + Arguments: + location: + Location of the directory to be created. + """ + try: + if not os.path.isdir(location): + os.makedirs(location) + except EnvironmentError as e: + if e.errno != errno.EEXIST: + raise GameTimeError("Cannot create directory located at %s: %s" % + (location, e)) + +def removeFile(location): + """Removes the file at the provided location. This is a wrapper around + the :func:`~os.remove` function of the :mod:`os` module, but does not + raise an exception if the file is not present. + + Arguments: + location: + Location of the file to be removed. + """ + try: + if os.path.exists(location): + os.remove(location) + except EnvironmentError as e: + raise GameTimeError("Cannot remove file located at %s: %s" % + (location, e)) + +def removeFiles(patterns, dirLocation): + """Removes the files from the directory whose location is provided, + whose names match any of the patterns in the list provided. + + Arguments: + patterns: + List of patterns to match filenames against. + dirLocation: + Location of the directory to remove files from. + """ + for filename in os.listdir(dirLocation): + for pattern in patterns: + if re.search(pattern, filename): + os.remove(os.path.join(dirLocation, filename)) + +def removeAllExcept(patterns, dirLocation): + """Removes all of the files and directories from the directory whose + location is provided, *except* for those files whose names match any + of the patterns in the list provided. + + Arguments: + patterns: + List of patterns to match filenames against. + dirLocation: + Location of the directory to remove files and + directories from. + """ + # Code from http://stackoverflow.com/a/1073382/1834042. + for root, dirs, files in os.walk(dirLocation): + for filename in files: + for pattern in patterns: + if not re.search(pattern, filename): + os.unlink(os.path.join(root, filename)) + for dirname in dirs: + shutil.rmtree(os.path.join(root, dirname)) + +def moveFiles(patterns, sourceDir, destDir, overwrite=True): + """Moves the files, whose names match any of the patterns in the list + provided, from the source directory whose location is provided to + the destination directory whose location is provided. If a file in + the destination directory has the same name as a file that is being moved + from the source directory, the former is overwritten if `overwrite` is + set to `True`; otherwise, the latter will not be moved. + + Arguments: + patterns: + List of patterns to match filenames against. + sourceDir: + Location of the source directory. + destDir: + Location of the destination directory. + overwrite: + Whether to overwrite a file in the destination directory that + has the same name as a file that is being moved from the source + directory. If `True`, the former is overwritten; if `False`, + the latter will not be moved. + """ + for filename in os.listdir(sourceDir): + for pattern in patterns: + if re.search(pattern, filename): + sourceFile = os.path.join(sourceDir, filename) + destFile = os.path.join(destDir, filename) + if overwrite and os.path.exists(destFile): + os.remove(destFile) + if overwrite or not os.path.exists(destFile): + shutil.move(sourceFile, destFile) diff --git a/src/file_helper.py b/src/file_helper.py deleted file mode 100644 index ffe4f7bb..00000000 --- a/src/file_helper.py +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env python - -"""Exposes miscellaneous functions to perform operations -on files and directories, such as creation, removal and movement. -""" -from typing import List - -"""See the LICENSE file, located in the root directory of -the source distribution and -at http://verifun.eecs.berkeley.edu/gametime/about/LICENSE, -for details on the GameTime license and authors. -""" - - -import errno -import os -import re -import shutil - -from gametime_error import GameTimeError - - -def create_dir(location: str) -> None: - """ - Creates the leaf directory in the path specified, along with any - intermediate-level directories needed to contain the directory. - This is a wrapper around the :func:`~os.makedirs` function of - the :mod:`os` module, but does not raise an exception if - the directory is already present, - - Parameters: - location: str : - Location of the directory to be created - - """ - try: - if not os.path.isdir(location): - os.makedirs(location) - except EnvironmentError as e: - if e.errno != errno.EEXIST: - raise GameTimeError("Cannot create directory located at %s: %s" % - (location, e)) - -def remove_file(location: str) -> None: - """ - Removes the file at the provided location. This is a wrapper around - the :func:`~os.remove` function of the :mod:`os` module, but does not - - Parameters: - location: str : - Location of the file to be removed - - """ - try: - if os.path.exists(location): - os.remove(location) - except EnvironmentError as e: - raise GameTimeError("Cannot remove file located at %s: %s" % - (location, e)) - -def remove_files(patterns: List[str], dir_location: str) -> None: - """ - Removes the files from the directory whose location is provided, - whose names match any of the patterns in the list provided. - - Parameters: - patterns: List[str] : - List of patterns to match filenames against - dir_location: str : - Location of the directory to remove files from - - """ - for filename in os.listdir(dir_location): - for pattern in patterns: - if re.search(pattern, filename): - os.remove(os.path.join(dir_location, filename)) - -def remove_all_except(patterns: List[str], dir_location: str) -> None: - """ - Removes all_temp_files of the files and directories from the directory whose - location is provided, *except* for those files whose names match any - of the patterns in the list provided. - - Parameters: - patterns: List[str] : - List of patterns to match filenames against - dir_location: str : - Location of the directory to remove files from - - """ - # Code from http://stackoverflow.com/a/1073382/1834042. - root: str - dirs: list[str] - files: list[str] - for root, dirs, files in os.walk(dir_location): - for filename in files: - for pattern in patterns: - if not re.search(pattern, filename): - os.unlink(os.path.join(root, filename)) - for dirname in dirs: - shutil.rmtree(os.path.join(root, dirname)) - -def move_files(patterns: List[str], source_dir: str, dest_dir: str, overwrite: bool = True) -> None: - """ - Moves the files, whose names match any of the patterns in the list - provided, from the source directory whose location is provided to - the destination directory whose location is provided. If a file in - the destination directory has the same name as a file that is being moved - from the source directory, the former is overwritten if `overwrite` is - set to `True`; otherwise, the latter will not be moved. - - Parameters: - patterns: List[str] : - List of patterns to match filenames against - source_dir: str : - Location of the source directory - dest_dir: str : - Location of the destination directory - overwrite: bool : - Whether to overwrite a file in the destination directory that has the same name as a file that is being moved from the source directory. (Default value = True) - - """ - for filename in os.listdir(source_dir): - for pattern in patterns: - if re.search(pattern, filename): - source_file: str = os.path.join(source_dir, filename) - dest_file: str = os.path.join(dest_dir, filename) - if overwrite and os.path.exists(dest_file): - os.remove(dest_file) - if overwrite or not os.path.exists(dest_file): - shutil.move(source_file, dest_file) \ No newline at end of file diff --git a/src/gametime_error.py b/src/gametimeError.py similarity index 80% rename from src/gametime_error.py rename to src/gametimeError.py index 4f5ee0c4..71c52b70 100644 --- a/src/gametime_error.py +++ b/src/gametimeError.py @@ -12,8 +12,3 @@ class GameTimeError(Exception): """Error that GameTime can throw.""" pass - - -class GameTimeWarning(Warning): - """Warning that GameTime can throw.""" - pass diff --git a/src/gametime_configuration.py b/src/gametime_configuration.py deleted file mode 100644 index ac22ddce..00000000 --- a/src/gametime_configuration.py +++ /dev/null @@ -1,143 +0,0 @@ -import os -from typing import Any -from yaml import load, dump - -try: - from yaml import CLoader as Loader, CDumper as Dumper -except ImportError: - from yaml import Loader, Dumper - -from gametime_error import GameTimeError - - -class Endianness(object): - """This class represents the endianness of the target machine.""" - # Big-endian. - BIG = 0 - # Little-endian. - LITTLE = 1 - - -class GametimeConfiguration(object): - """Stores information necessary to configure GameTime.""" - - def __init__(self): - """Constructor for the GametimeConfiguration class.""" - - ### GAMETIME INFORMATION ### - # URL of the website for GameTime. - self.WEBSITE_URL: str = "" - - # Current version number of GameTime. - self.VERSION: str = "" - - # URL that provides information about - # the latest version of GameTime. - self.LATEST_VERSION_INFO_URL: str = "" - - ### FILE INFORMATION ### - # Full location of the configuration file. - self.config_file: str = "" - - # Directory that contains the configuration file. - self.config_dir: str = "" - - ### MEMORY LAYOUT INFORMATION ### - # Word size on the machine that GameTime is being run on (in bits). - # This value should be changed if GameTime will be run on a - # non-32-bit machine. - self.WORD_BITSIZE: int = 32 - - # Word size on the machine that GameTime is being run on (in bytes). - # This value should be changed and the solution should be recompiled, - # if GameTime will be run on a non-32-bit machine. - self.WORD_BYTESIZE: int = 4 - - # Endianness of the target machine. - self.ENDIANNESS: int = Endianness.LITTLE - - ### ANNOTATIONS ### - # Annotation that is used when additional conditions need to be - # provided to GameTime. - self.ANNOTATION_ASSUME: str = "" - - # Annotation that is used when a simulation is performed. - self.ANNOTATION_SIMULATE: str = "" - - ### SPECIAL IDENTIFIERS ### - # The special identifiers and for the names and prefixes of temporary - # files and folders are described in the default GameTime - # configuration XML file provided in the source directory. - self.IDENT_AGGREGATE: str = "" - self.IDENT_CONSTRAINT: str = "" - self.IDENT_EFC: str = "" - self.IDENT_FIELD: str = "" - self.IDENT_TEMPINDEX: str = "" - self.IDENT_TEMPPTR: str = "" - self.IDENT_TEMPVAR: str = "" - - self.TEMP_PROJECT_CONFIG: str = "" - self.TEMP_MERGED: str = "" - self.TEMP_LOOP_CONFIG: str = "" - - self.TEMP_SUFFIX: str = "" - - self.TEMP_SUFFIX_MERGED: str = "" - self.TEMP_SUFFIX_UNROLLED: str = "" - self.TEMP_SUFFIX_INLINED: str = "" - self.TEMP_SUFFIX_LINE_NUMS: str = "" - - self.TEMP_PHX_CREATE_DAG: str = "" - self.TEMP_DAG: str = "" - self.TEMP_DAG_ID_MAP: str = "" - - self.TEMP_PHX_IR: str = "" - self.TEMP_PHX_FIND_CONDITIONS: str = "" - - self.TEMP_PATH_ILP_PROBLEM: str = "" - self.TEMP_PATH_NODES: str = "" - self.TEMP_PATH_CONDITIONS: str = "" - self.TEMP_PATH_CONDITION_EDGES: str = "" - self.TEMP_PATH_CONDITION_TRUTHS: str = "" - self.TEMP_PATH_LINE_NUMBERS: str = "" - self.TEMP_PATH_ARRAY_ACCESSES: str = "" - self.TEMP_PATH_AGG_INDEX_EXPRS: str = "" - self.TEMP_PATH_PREDICTED_VALUE: str = "" - self.TEMP_PATH_MEASURED_VALUE: str = "" - self.TEMP_PATH_ALL: str = "" - - self.TEMP_PATH_QUERY: str = "" - self.TEMP_PATH_QUERY_ALL: str = "" - - self.TEMP_CASE: str = "" - - self.TEMP_BASIS_MATRIX: str = "" - self.TEMP_MEASUREMENT: str = "" - self.TEMP_BASIS_VALUES: str = "" - self.TEMP_DAG_WEIGHTS: str = "" - self.TEMP_DISTRIBUTION: str = "" - - -def read_gametime_config_yaml(yaml_config_path: str) -> GametimeConfiguration: - """ - Creates GametimeConfiguration from yaml files - - Parameters: - yaml_config_path: str : - path of the yaml config file that contains - - Returns: - GametimeConfiguration - GametimeConfiguration object that contains information from YAML file at yaml_config_path - - """ - # Check file exists - if not os.path.exists(yaml_config_path): - err_msg: str = "Cannot find gametime configuration file: %s" % yaml_config_path - raise GameTimeError(err_msg) - - # Initialize new GametimeConfiguration - with open(yaml_config_path) as raw_gametime_file: - gametime_confg: GametimeConfiguration = load(raw_gametime_file, Loader=Loader) - - return gametime_confg diff --git a/src/histogram.py b/src/histogram.py index 1e431a94..24cce6cc 100644 --- a/src/histogram.py +++ b/src/histogram.py @@ -3,7 +3,6 @@ """Exposes functions to create, and interact with, a histogram computed from the values of feasible paths generated by GameTime. """ -from path import Path """See the LICENSE file, located in the root directory of the source distribution and @@ -15,66 +14,74 @@ import numpy as np from defaults import logger -from gametime_error import GameTimeError +from gametimeError import GameTimeError -def compute_histogram(paths: list[Path], bins=10, path_value_range=None, measured=False): - """ - Computes a histogram from the values of a list of +def computeHistogram(paths, bins=10, range=None, measured=False): + """Computes a histogram from the values of a list of feasible paths generated by GameTime. This function is a wrapper around the function :func:`~numpy.histogram` from the module :mod:`numpy`. Refer to the documentation of this function for more information about the computed histogram. - Parameters: - paths: list[Path] : - List of feasible paths generated by GameTime, each represented by a `~gametime.src.path.Path` object. + Arguments: + paths: + List of feasible paths generated by GameTime, each + represented by a :class:`~gametime.path.Path` object. bins: - Same purpose as the same-named argument of the function :func:`numpy.histogram`. - path_value_range: - Same purpose as the range argument of the function :func:`numpy.histogram`. (Default value = None) - measured : - `True` if, and only if, the values that will be used for the histogram are the measured values of the feasible paths. (Default value = False) + Same purpose as the same-named argument of the function + :func:`numpy.histogram`. + range: + Same purpose as the same-named argument of the function + :func:`numpy.histogram`. + measured: + `True` if, and only if, the values that will be used for + the histogram are the measured values of the feasible paths. Returns: - Tuple - Tuple, whose first element is an array of the values of the histogram, and whose second element is an array of the left edges of the bins. - + Tuple, whose first element is an array of the values of + the histogram, and whose second element is an array of + the left edges of the bins. """ - path_values = [path.measured_value if measured else path.predicted_value + pathValues = [path.measuredValue if measured else path.predictedValue for path in paths] - if path_value_range is None: - path_value_range = (min(path_values), max(path_values)) - return np.histogram(path_values, bins=bins, range=path_value_range) + return np.histogram(pathValues, bins=bins, range=range) -def write_histogram_to_file(location, paths, bins=10, path_value_range=None, measured=False): - """ - Create a histogram and write it to location. +def writeHistogramToFile(location, paths, bins=10, range=None, measured=False): + """Computes a histogram from the values of a list of + feasible paths generated by GameTime, and writes the histogram + to a file. Each line of the file has the left edge of each bin + and the number of samples in each bin, with both of the values + separated by whitespace. - Parameters: - location : - The stored folder for histogram file - paths: list[Path] : - List of feasible paths generated by GameTime, each represented by a `~gametime.src.path.Path` object. + Arguments: + location: + Location of the file. + paths: + List of feasible paths generated by GameTime, each + represented by a :class:`~gametime.path.Path` object. bins: - Same purpose as the same-named argument of the function :func:`numpy.histogram`. - path_value_range: - Same purpose as the range argument of the function :func:`numpy.histogram`. (Default value = None) - measured : - `True` if, and only if, the values that will be used for the histogram are the measured values of the feasible paths. (Default value = False) + Same purpose as the same-named argument of the function + :func:`numpy.histogram`. + range: + Same purpose as the same-named argument of the function + :func:`numpy.histogram`. + measured: + `True` if, and only if, the values that will be used for + the histogram are the measured values of the feasible paths. """ logger.info("Creating histogram...") - hist, bin_edges = compute_histogram(paths, bins, path_value_range, measured) + hist, binEdges = computeHistogram(paths, bins, range, measured) try: - histogram_file_handler = open(location, "w") + histogramFileHandler = open(location, "w") except EnvironmentError as e: - err_msg = ("Error writing the histogram to the file located " + errMsg = ("Error writing the histogram to the file located " "at %s: %s" % (location, e)) - raise GameTimeError(err_msg) + raise GameTimeError(errMsg) else: - with histogram_file_handler: - for binEdge, sample in zip(bin_edges, hist): - histogram_file_handler.write("%s\t%s\n" % (binEdge, sample)) + with histogramFileHandler: + for binEdge, sample in zip(binEdges, hist): + histogramFileHandler.write("%s\t%s\n" % (binEdge, sample)) logger.info("Histogram created.") diff --git a/src/index_expression.py b/src/indexExpression.py similarity index 65% rename from src/index_expression.py rename to src/indexExpression.py index ade6924c..7dc2c176 100644 --- a/src/index_expression.py +++ b/src/indexExpression.py @@ -3,7 +3,6 @@ """Defines a class that maintains information about an expression associated with a temporary index variable. """ -from typing import Tuple """See the LICENSE file, located in the root directory of the source distribution and @@ -15,19 +14,32 @@ class IndexExpression(object): """Maintains information about an expression associated with a temporary index variable. + + Attributes: + name: + Name of the variable in the expression. + indices: + Tuple of temporary index numbers used as + indices in the expression. """ - def __init__(self, name: str, indices: Tuple[int]): - self.name: str = name - self.indices: Tuple[int] = indices + def __init__(self, name, indices): + self.name = name + self.indices = indices - def get_name(self) -> str: - """Name of the variable in the expression whose information is stored in this object. + def getName(self): + """ + Returns: + Name of the variable in the expression + whose information is stored in this object. """ return self.name - def get_indices(self) -> Tuple[int]: - """Tuple of the temporary index numbers used as indices in the expression. + def getIndices(self): + """ + Returns: + Tuple of the temporary index numbers used + as indices in the expression. """ return self.indices @@ -37,7 +49,7 @@ def __eq__(self, other): return False def __str__(self): - result: str = self.name + result = self.name for index in self.indices: result = "%s %s" % (result, index) return result.strip() @@ -47,6 +59,10 @@ class VariableIndexExpression(IndexExpression): """Maintains information about an expression associated with a temporary index variable, where the expression represents a variable. + + Attributes: + name: + Name of the variable in the expression. """ def __init__(self, name): IndexExpression.__init__(self, name, []) diff --git a/src/inliner.py b/src/inliner.py index 92a7810f..2f4a688b 100644 --- a/src/inliner.py +++ b/src/inliner.py @@ -1,74 +1,81 @@ +#!/usr/bin/env python + +"""Exposes functions to perform a source-to-source transformation that +inlines user-specified functions in the C file currently being analyzed. +""" + +"""See the LICENSE file, located in the root directory of +the source distribution and +at http://verifun.eecs.berkeley.edu/gametime/about/LICENSE, +for details on the GameTime license and authors. +""" + + import os -import re import subprocess -import sys - -def run_command(command): - result = subprocess.run(command, shell=True, capture_output=True, text=True, errors='replace') - if result.returncode != 0: - print(f"Error running command: {command}") - print(result.stdout) - print(result.stderr) - sys.exit(1) - return result.stdout +from defaults import config, sourceDir -def link_bitcode(bitcode_files, output_file): - run_command(f"llvm-link {' '.join(bitcode_files)} -o {output_file}") -def disassemble_bitcode(input_file, output_file): - run_command(f"llvm-dis {input_file} -o {output_file}") +def _generateInlinerCommand(projectConfig): + """Generates the system call that runs the CIL inliner with + appropriate inputs. -def assemble_bitcode(input_file, output_file): - run_command(f"llvm-as {input_file} -o {output_file}") + Arguments: + projectConfig: + :class:`~gametime.projectConfiguration.ProjectConfiguration` + object that represents the configuration of a GameTime project. -def inline_bitcode(input_file, output_file): - run_command(f"opt -passes=\"always-inline,inline\" -inline-threshold=10000000 {input_file} -o {output_file}") + Returns: + Appropriate system call as a list that contains the program + to be run and the proper arguments. + """ + # Set the environment variable that allows the Cilly driver to find + # the path to the configuration file for the Findlib OCaml module. + os.environ["OCAMLFIND_CONF"] = os.path.join(sourceDir, + "ocaml/conf/findlib.conf") -def modify_llvm_ir(input_file, output_file, skip_function): - current_dir = os.path.dirname(os.path.abspath(__file__)) - plugin_path = os.path.normpath(os.path.join(current_dir, "../src/custom_passes/custom_inline_pass.so")) + # Set the environment variable that allows the Cilly driver to find + # the path to the folder that contains the compiled OCaml files. + os.environ["OCAMLPATH"] = os.path.join(sourceDir, "ocaml/lib") - if not os.path.exists(plugin_path): - print(plugin_path) - print("Plugin Not Found") - sys.exit(1) + # Set the environment variable that configures the Cilly driver to load + # the features that will be needed for the inliner. + os.environ["CIL_FEATURES"] = "cil.default-features,cil.inliner" - run_command(f"opt -load-pass-plugin={plugin_path} -passes=custom-inline -analysed-func={skip_function} {input_file} -o {output_file} -S") + command = [] + command.append(os.path.join(config.TOOL_CIL, "bin/cilly.bat")) + inlined = projectConfig.inlined + for funcName in inlined: + command.append("--inline=%s" % funcName) -def inline_functions(bc_filepaths: list[str], output_file_folder: str, output_name: str, analyzed_function: str) -> str: - output_file: str = os.path.join(output_file_folder, f"{output_name}.bc") - file_to_analyze = bc_filepaths[0] - combined_bc = f"{file_to_analyze[:-3]}_linked.bc" - combined_ll = f"{file_to_analyze[:-3]}_linked.ll" - combined_mod_ll = f"{file_to_analyze[:-3]}_linked_mod.ll" - combined_mod_bc = f"{file_to_analyze[:-3]}_linked_mod.bc" - combined_inlined_mod_bc = f"{file_to_analyze[:-3]}_linked_inlined_mod.bc" - combined_inlined_mod_ll = f"{file_to_analyze[:-3]}_linked_inlined_mod.ll" + command.append(projectConfig.locationTempFile) + command.append("-I'%s'" % projectConfig.locationOrigDir) + for includePath in projectConfig.included: + command.append("-I'%s'" % includePath) - - if len(bc_filepaths) > 1: - # Step 1: Link all bitcode files into a single combined bitcode file - link_bitcode(bc_filepaths, combined_bc) - else: - combined_bc = bc_filepaths[0] + command.append("--save-temps='%s'" % projectConfig.locationTempDir) + command.append("-c") + command.append("-o") + command.append("'%s.out'" % projectConfig.locationTempNoExtension) - # Step 2: Disassemble the combined bitcode file to LLVM IR - disassemble_bitcode(combined_bc, combined_ll) + return command - # Step 3: Modify the LLVM IR file - modify_llvm_ir(combined_ll, combined_mod_ll, analyzed_function) +def runInliner(projectConfig): + """Conducts the sequence of system calls that will perform + a source-to-source transformation of the C file currently being + analyzed, inlining user-specified functions. - # Step 4: Assemble the modified LLVM IR back to bitcode - assemble_bitcode(combined_mod_ll, combined_mod_bc) + Arguments: + projectConfig: + :class:`~gametime.projectConfiguration.ProjectConfiguration` + object that represents the configuration of a GameTime project. - # Step 5: Inline the functions in the modified bitcode - inline_bitcode(combined_mod_bc, combined_inlined_mod_bc) - - # Step 6: Disassemble the combined bitcode file to LLVM IR for debugging - disassemble_bitcode(combined_inlined_mod_bc, combined_inlined_mod_ll) - - return combined_inlined_mod_bc + Returns: + Zero if the inlining was successful; a non-zero value otherwise. + """ + command = _generateInlinerCommand(projectConfig) + return subprocess.call(command, shell=True) diff --git a/src/interval.py b/src/interval.py index c1cd6ea6..395304e1 100644 --- a/src/interval.py +++ b/src/interval.py @@ -14,13 +14,23 @@ class Interval(object): """Maintains a representation of, and information about, an interval of values. + + Attributes: + lower: + Lower bound of the interval. The lower bound is itself + included in the interval. If this attribute is `None`, + the interval has no finite lower bound. + upper: + Upper bound of the interval. The upper bound is itself + included in the interval. If this attribute is `None`, + the interval has no finite upper bound. """ - def __init__(self, lower: int=None, upper: int=None): + def __init__(self, lower=None, upper=None): if lower is not None and upper is not None: lower, upper = sorted((lower, upper)) - self.lower: int = lower - self.upper: int = upper + self.lower = lower + self.upper = upper def __str__(self): return ("%s%s, %s%s" % @@ -30,33 +40,31 @@ def __str__(self): ")" if self.upper is None else "]")) @property - def lower_bound(self) -> int: - """ - Returns: - Lower bound of the interval represented by this object, or `None` if the interval has no finite lower bound. + def lowerBound(self): + """Lower bound of the interval represented by this object, + or `None` if the interval has no finite lower bound. """ return self.lower @property - def upper_bound(self) -> int: - """ - Returns: - Upper bound of the interval represented by this object, or `None` if the interval has no finite upper bound. + def upperBound(self): + """Upper bound of the interval represented by this object, + or `None` if the interval has no finite upper bound. """ return self.upper - def has_finite_lower_bound(self) -> bool: + def hasFiniteLowerBound(self): """ Returns: - `True` if, and only if, the represented interval has a finite lower bound. - + `True` if, and only if, the represented interval has + a finite lower bound. """ return self.lower is not None - def has_finite_upper_bound(self) -> bool: + def hasFiniteUpperBound(self): """ Returns: - `True` if, and only if, the represented interval has a finite upper bound. - + `True` if, and only if, the represented interval has + a finite upper bound. """ return self.upper is not None diff --git a/src/legacy_inliner.py b/src/legacy_inliner.py deleted file mode 100644 index 12b5608a..00000000 --- a/src/legacy_inliner.py +++ /dev/null @@ -1,115 +0,0 @@ -import os -import re -import subprocess -import sys - -def run_command(command): - result = subprocess.run(command, shell=True, capture_output=True, text=True, errors='replace') - if result.returncode != 0: - print(f"Error running command: {command}") - print(result.stdout) - print(result.stderr) - sys.exit(1) - return result.stdout - -def link_bitcode(bitcode_files, output_file): - run_command(f"llvm-link {' '.join(bitcode_files)} -o {output_file}") - -def disassemble_bitcode(input_file, output_file): - run_command(f"llvm-dis {input_file} -o {output_file}") - -def modify_llvm_ir(input_file, output_file, skip_function): - # Read the LLVM IR code from the file - with open(input_file, 'r') as f: - llvm_ir = f.read() - - # Split the content by lines for easier processing - lines = llvm_ir.splitlines() - - # Dictionary to map function tags to whether they should be skipped - skip_tags = set() - - # Process each line - modified_lines = [] - for i in range(len(lines)): - line = lines[i] - - # Check if the line is a function definition - func_match = re.match(r'define\s+\S+\s+@\S+\s*\(.*\)\s*(#\d+)\s*{', line) - if func_match: - func_tag = func_match.group(1) - if f'@{skip_function}(' in line: - # If this function is the one to skip, record its tag - skip_tags.add(func_tag) - else: - # Replace 'noinline' with 'alwaysinline' if not skipping - if 'noinline' in lines[i-1]: # Check previous line for noinline - modified_lines[-1] = modified_lines[-1].replace('noinline', 'alwaysinline') - - # Add the processed line to the list of modified lines - modified_lines.append(line) - - # Second pass: Modify the attributes section - final_lines = [] - for line in modified_lines: - # Match the attributes definition - attr_match = re.match(r'attributes\s+(#\d+)\s*=\s*{', line) - if attr_match: - attr_tag = attr_match.group(1) - if attr_tag not in skip_tags: - # Replace 'noinline' with 'alwaysinline' in the attributes if not skipping - line = line.replace('noinline', 'alwaysinline') - final_lines.append(line) - - # Join the final lines back into a single string - modified_llvm_ir = '\n'.join(final_lines) - - # Write the modified LLVM IR back to the output file - with open(output_file, 'w') as f: - f.write(modified_llvm_ir) - -def assemble_bitcode(input_file, output_file): - run_command(f"llvm-as {input_file} -o {output_file}") - -def inline_bitcode(input_file, output_file): - run_command(f"opt -passes=\"always-inline,inline\" -inline-threshold=10000000 {input_file} -o {output_file}") - -def generate_cfg(input_file): - run_command(f"opt -dot-cfg {input_file}") - - -def inline_functions(bc_filepaths: list[str], output_file_folder: str, output_name: str, analyzed_function: str) -> str: - output_file: str = os.path.join(output_file_folder, f"{output_name}.bc") - file_to_analyze = bc_filepaths[0] - combined_bc = f"{file_to_analyze[:-3]}_linked.bc" - combined_ll = f"{file_to_analyze[:-3]}_linked.ll" - combined_mod_ll = f"{file_to_analyze[:-3]}_linked_mod.ll" - combined_mod_bc = f"{file_to_analyze[:-3]}_linked_mod.bc" - combined_inlined_mod_bc = f"{file_to_analyze[:-3]}_linked_inlined_mod.bc" - combined_inlined_mod_ll = f"{file_to_analyze[:-3]}_linked_inlined_mod.ll" - - - - if len(bc_filepaths) > 1: - # Step 1: Link all bitcode files into a single combined bitcode file - link_bitcode(bc_filepaths, combined_bc) - else: - combined_bc = bc_filepaths[0] - - # Step 2: Disassemble the combined bitcode file to LLVM IR - disassemble_bitcode(combined_bc, combined_ll) - - # Step 3: Modify the LLVM IR file - modify_llvm_ir(combined_ll, combined_mod_ll, analyzed_function) - - # Step 4: Assemble the modified LLVM IR back to bitcode - assemble_bitcode(combined_mod_ll, combined_mod_bc) - - # Step 5: Inline the functions in the modified bitcode - inline_bitcode(combined_mod_bc, combined_inlined_mod_bc) - - # Step 6: Disassemble the combined bitcode file to LLVM IR for debugging - disassemble_bitcode(combined_inlined_mod_bc, combined_inlined_mod_ll) - - return combined_inlined_mod_bc - diff --git a/src/loggingHelper.py b/src/loggingHelper.py new file mode 100644 index 00000000..2b1896a7 --- /dev/null +++ b/src/loggingHelper.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python + +"""Exposes functions to interact with, and modify an object of, +the :mod:`logging` Python class. +""" + +"""See the LICENSE file, located in the root directory of +the source distribution and +at http://verifun.eecs.berkeley.edu/gametime/about/LICENSE, +for details on the GameTime license and authors. +""" + + +import logging +import sys + + +def initialize(logger): + """Initializes the logger provided with + :class:`~logging.Formatter` and :class:`~logging.StreamHandler` + objects appropriate for GameTime. + + Arguments: + logger: + Logger to initialize. + """ + logger.setLevel(logging.DEBUG) + logger.propagate = False + + formatter = logging.Formatter("%(message)s") + + stdoutHandler = logging.StreamHandler(sys.stdout) + stdoutHandler.setLevel(logging.INFO) + stdoutHandler.setFormatter(formatter) + logger.addHandler(stdoutHandler) + + stderrHandler = logging.StreamHandler(sys.stderr) + stderrHandler.setLevel(logging.ERROR) + stderrHandler.setFormatter(formatter) + logger.addHandler(stderrHandler) diff --git a/src/logging_helper.py b/src/logging_helper.py deleted file mode 100644 index bfe3fac0..00000000 --- a/src/logging_helper.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python - -"""Exposes functions to interact with, and modify an object of, -the :mod:`logging` Python class. -""" - -"""See the LICENSE file, located in the root directory of -the source distribution and -at http://verifun.eecs.berkeley.edu/gametime/about/LICENSE, -for details on the GameTime license and authors. -""" - - -import logging -import sys - - -def initialize(logger: logging.Logger) -> None: - """ - Initializes the logger provided with - `~logging.Formatter` and `~logging.StreamHandler` - objects appropriate for GameTime. - - Parameters: - logger: logging.Logger : - Logger to initialize - """ - logger.setLevel(logging.DEBUG) - logger.propagate = False - - formatter = logging.Formatter("%(message)s") - - stdout_handler = logging.StreamHandler(sys.stdout) - stdout_handler.setLevel(logging.INFO) - stdout_handler.setFormatter(formatter) - logger.addHandler(stdout_handler) - - stderr_handler = logging.StreamHandler(sys.stderr) - stderr_handler.setLevel(logging.ERROR) - stderr_handler.setFormatter(formatter) - logger.addHandler(stderr_handler) diff --git a/src/loopHandler.py b/src/loopHandler.py new file mode 100644 index 00000000..38d140e5 --- /dev/null +++ b/src/loopHandler.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python + +"""Exposes functions to perform a source-to-source transformation +that detects and unrolls loops in the code being analyzed. +""" + +"""See the LICENSE file, located in the root directory of +the source distribution and +at http://verifun.eecs.berkeley.edu/gametime/about/LICENSE, +for details on the GameTime license and authors. +""" + + +import os +import subprocess + +from defaults import config, sourceDir + + +class HandlerMode(object): + """Represents the mode that the loop handler works in.""" + + #: Detect loops. + DETECTOR = 0 + #: Unroll loops. + UNROLLER = 1 + + +def _generateHandlerCommand(projectConfig, handlerMode): + """Generates the system call that runs the loop handler + with appropriate inputs. + + Arguments: + projectConfig: + :class:`~gametime.projectConfiguration.ProjectConfiguration` + object that represents the configuration of a GameTime project. + handlerMode: + Mode that the loop handler should run in. + + Returns: + Appropriate system call as a list that contains the program + to be run and the proper arguments. + """ + # Set the environment variable that allows the Cilly driver to find + # the path to the configuration file for the Findlib OCaml module. + os.environ["OCAMLFIND_CONF"] = os.path.join(sourceDir, + "ocaml/conf/findlib.conf") + + # Set the environment variable that allows the Cilly driver to find + # the path to the folder that contains the compiled OCaml files. + os.environ["OCAMLPATH"] = os.path.join(sourceDir, "ocaml/lib") + + # Set the environment variable that configures the Cilly driver to load + # the features that will be needed for the loop handler. + os.environ["CIL_FEATURES"] = "cil.default-features,loopHandler.loopHandler" + + command = [] + + command.append(os.path.join(config.TOOL_CIL, "bin/cilly.bat")) + + command.append("--doloopHandler") + command.append("--loopHandler-detect" + if handlerMode is HandlerMode.DETECTOR + else "--loopHandler-unroll") + + command.append("--loopHandler-analyze=%s" % projectConfig.func) + loopConfigFile = os.path.join(projectConfig.locationTempDir, + config.TEMP_LOOP_CONFIG) + command.append("--loopHandler-config='%s'" % loopConfigFile) + + for inlineName in projectConfig.inlined: + command.append("--inline='%s'" % inlineName) + + analysisFile = ("%s%s.c" % (projectConfig.locationTempNoExtension, + config.TEMP_SUFFIX_LINE_NUMS) + if handlerMode is HandlerMode.DETECTOR + else projectConfig.locationTempFile) + command.append(analysisFile) + + command.append("-I'%s'" % projectConfig.locationOrigDir) + command.append("--save-temps='%s'" % projectConfig.locationTempDir) + command.append("-c") + command.append("-o") + command.append("'%s.out'" % projectConfig.locationTempNoExtension) + + return command + +def runDetector(projectConfig): + """Conducts the sequence of system calls that will detect loops + for the function currently being analyzed. The output of the + detector will be placed in a loop configuration file that the + user has to modify: this file contains the line numbers of each + loop header, and the user has to specify bounds for each loops + by changing the number beside the line numbers, which is set to + 1 by default. + + Arguments: + projectConfig: + :class:`~gametime.projectConfiguration.ProjectConfiguration` + object that represents the configuration of a GameTime project. + + Returns: + Zero if the inlining was successful; a non-zero value otherwise. + """ + command = _generateHandlerCommand(projectConfig, HandlerMode.DETECTOR) + proc = subprocess.call(command, shell=True) + return proc + +def runUnroller(projectConfig): + """Conducts the sequence of system calls that will unroll loops + in the function currently being analyzed. The output of the + detector will be a temporary file for GameTime analysis where + all of the loops have been unrolled using user-specified bounds. + + Precondition: The loop detector has already been run, and the user + has already specified bounds for each loop in the loop configuration + file generated by the loop detector. + + Arguments: + projectConfig: + :class:`~gametime.projectConfiguration.ProjectConfiguration` + object that represents the configuration of a GameTime project. + + Returns: + Zero if the inlining was successful; a non-zero value otherwise. + """ + command = _generateHandlerCommand(projectConfig, HandlerMode.UNROLLER) + proc = subprocess.call(command, shell=True) + return proc diff --git a/src/merger.py b/src/merger.py new file mode 100644 index 00000000..ac10d22b --- /dev/null +++ b/src/merger.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python + +"""Exposes functions to perform a source-to-source transformation +that merges the source file, which contains the code under analysis, +with other user-specified external files. +""" + +"""See the LICENSE file, located in the root directory of +the source distribution and +at http://verifun.eecs.berkeley.edu/gametime/about/LICENSE, +for details on the GameTime license and authors. +""" + + +import os +import subprocess + +from defaults import config, sourceDir +from gametimeError import GameTimeError + + +def _generateOtherSourcesFile(projectConfig): + """Creates a file that contains the paths of other files to be + merged with the source file. + + Arguments: + projectConfig: + :class:`~gametime.projectConfiguration.ProjectConfiguration` + object that represents the configuration of a GameTime project. + + Returns: + Path to the file that contains the paths of other files + to be merged with the source file. + """ + otherSourcesFilePath = os.path.join(projectConfig.locationTempDir, + config.TEMP_MERGED) + + # Create the file. + try: + otherSourcesFileHandler = open(otherSourcesFilePath, "w") + except EnvironmentError as e: + errMsg = ("Error creating a temporary file located at %s, " + "which stores the paths of the other files to be " + "merged with the source file: %s" % + (otherSourcesFilePath, e)) + raise GameTimeError(errMsg) + else: + with otherSourcesFileHandler: + for filePath in projectConfig.merged: + otherSourcesFileHandler.write("%s\n" % filePath) + + return otherSourcesFilePath + +def _generateMergerCommand(projectConfig, otherSourcesFilePath): + """Generates the system call that performs a source-to-source + transformation to merge the source file, which contains + the code under analysis, with other user-specified external files. + + Arguments: + projectConfig: + :class:`~gametime.projectConfiguration.ProjectConfiguration` + object that represents the configuration of a GameTime project. + otherSourcesFilePath: + Path to the file that contains the paths to + user-specified external files. + + Returns: + Appropriate system call as a list that contains the program + to be run and the proper arguments. + """ + # Set the environment variable that allows the Cilly driver to find + # the path to the configuration file for the Findlib OCaml module. + os.environ["OCAMLFIND_CONF"] = os.path.join(sourceDir, + "ocaml/conf/findlib.conf") + + # Set the environment variable that allows the Cilly driver to find + # the path to the directory that contains the compiled OCaml files. + os.environ["OCAMLPATH"] = os.path.join(sourceDir, "ocaml/lib") + + command = [] + + command.append(os.path.join(config.TOOL_CIL, "bin/cilly.bat")) + + command.append("--merge") + command.append("--extrafiles='%s'" % otherSourcesFilePath) + + command.append(projectConfig.locationTempFile) + + command.append("-I'%s'" % projectConfig.locationOrigDir) + for includePath in projectConfig.included: + command.append("-I'%s'" % includePath) + + command.append("--save-temps='%s'" % projectConfig.locationTempDir) + command.append("-o") + command.append("'%s.out'" % projectConfig.locationTempNoExtension) + + return command + +def runMerger(projectConfig): + """Conducts the sequence of system calls that will perform + a source-to-source transformation to merge the source file, + which contains the code under analysis, with other user-specified + external files. + + Arguments: + projectConfig: + :class:`~gametime.projectConfiguration.ProjectConfiguration` + object that represents the configuration of a GameTime project. + + Returns: + Zero if the merging was successful; a non-zero value otherwise. + """ + # Trigger the environment variable that prevents Cilly from + # compiling and linking the file that is produced by merging. + os.environ["CILLY_DONT_COMPILE_AFTER_MERGE"] = "1" + + otherSourcesFilePath = _generateOtherSourcesFile(projectConfig) + command = _generateMergerCommand(projectConfig, otherSourcesFilePath) + return subprocess.call(command, shell=True) diff --git a/src/nxHelper.py b/src/nxHelper.py new file mode 100644 index 00000000..34ea18db --- /dev/null +++ b/src/nxHelper.py @@ -0,0 +1,359 @@ +#!/usr/bin/env python + +"""Exposes classes and functions to supplement those provided by +the NetworkX graph package. +""" + +"""See the LICENSE file, located in the root directory of +the source distribution and +at http://verifun.eecs.berkeley.edu/gametime/about/LICENSE, +for details on the GameTime license and authors. +""" + + +import os + +from os import linesep as lsep +from random import randrange + +import networkx as nx + +from defaults import logger +from gametimeError import GameTimeError + + +class Dag(nx.DiGraph): + """Maintains information about the directed acyclic graph (DAG) + of the code being analyzed. It is a subclass of + the :class:`~networkx.DiGraph` class of the NetworkX graph package, + and stores additional information relevant to the GameTime analysis, + such as the special 'default' edges in the DAG. + """ + + def __init__(self, *args, **kwargs): + super(Dag, self).__init__(*args, **kwargs) + + #: Source of the DAG. + self.source = "" + + #: Sink of the DAG. + self.sink = "" + + #: Number of nodes in the DAG. + self.numNodes = 0 + + #: Number of edges in the DAG. + self.numEdges = 0 + + #: Number of paths in the DAG. + self.numPaths = 0 + + #: List of nodes in the DAG. + self.allNodes = [] + + #: Dictionary that maps nodes to their indices in the list of all nodes. + #: This is maintained for efficiency purposes. + self.nodesIndices = {} + + #: List of nodes in the DAG that are not the sink. + #: We assume that there is only one sink. + #: This is maintained for efficiency purposes. + self.nodesExceptSink = [] + + #: Dictionary that maps nodes (except the sink) to their indices in + #: the list of all nodes (except the sink). This is maintained for + #: efficiency purposes. + self.nodesExceptSinkIndices = {} + + #: List of nodes in the DAG that are neither the source nor the sink. + #: We assume that there is only one source and one sink. This is + #: maintained for efficiency purposes. + self.nodesExceptSourceSink = [] + + #: Dictionary that maps nodes (except the source and the sink) to their + #: indices in the list of all nodes (except the source and the sink). + #: This is maintained for efficiency purposes. + self.nodesExceptSourceSinkIndices = {} + + #: List of edges in the DAG. + self.allEdges = [] + + #: Dictionary that maps edges to their indices in the list of all edges. + #: This is maintained for efficiency purposes. + self.edgesIndices = {} + + #: List of non-special edges in the DAG. + self.edgesReduced = [] + + #: Dictionary that maps non-special edges to their indices in the + #: list of all edges. This is maintained for efficiency purposes. + self.edgesReducedIndices = {} + + #: Dictionary that maps nodes to the special ('default') edges. + self.specialEdges = {} + + #: List of the weights assigned to the edges in the DAG, arranged + #: in the same order as the edges are in the list of all edges. + self.edgeWeights = [] + + def initializeDictionaries(self): + self.numNodes = self.number_of_nodes() + self.numEdges = self.number_of_edges() + self.allNodes = nodes = sorted(self.nodes(), key=int) + self.allEdges = self.edges() + + # We assume there is only one source and one sink. + self.source = [node for node in nodes if self.in_degree(node) == 0][0] + self.sink = [node for node in nodes if self.out_degree(node) == 0][0] + self.nodesExceptSink = [node for node in self.allNodes + if node != self.sink] + self.nodesExceptSourceSink = [node for node in self.allNodes + if node != self.source and + node != self.sink] + + self.numPaths = (0 if hasCycles(self) else + numPaths(self, self.source, self.sink)) + + + # Initialize dictionaries that map nodes and edges to their indices + # in the node list and edge list, respectively. + for nodeIndex, node in enumerate(self.allNodes): + self.nodesIndices[node] = nodeIndex + for nodeIndex, node in enumerate(self.nodesExceptSink): + self.nodesExceptSinkIndices[node] = nodeIndex + for nodeIndex, node in enumerate(self.nodesExceptSourceSink): + self.nodesExceptSourceSinkIndices[node] = nodeIndex + for edgeIndex, edge in enumerate(self.allEdges): + self.edgesIndices[edge] = edgeIndex + + + def loadVariables(self): + """Loads the instance variables of this object with appropriate + values. This method is useful when the DAG is loaded from a DOT file. + """ + self.initializeDictionaries() + self.resetEdgeWeights() + + logger.info("Initializing data structures for special edges...") + self._initSpecialEdges() + logger.info("Data structures initialized.") + + def resetEdgeWeights(self): + """Resets the weights assigned to the edges of the DAG.""" + self.edgeWeights = [0] * self.numEdges + + def _initSpecialEdges(self): + """To reduce the dimensionality to b = n-m+2, each node, except for + the source and sink, chooses a 'special' edge. This edge is taken if + flow enters the node, but no outgoing edge is 'visibly' selected. + In other words, it is the 'default' edge for the node. + + This method initializes all of the data structures necessary + to keep track of these special edges. + """ + self.edgesReduced = list(self.allEdges) + + for node in self.nodesExceptSourceSink: + out_edges = self.out_edges(node) + if len(out_edges) > 0: + # By default, we pick the first edge as 'special'. + self.specialEdges[node] = out_edges[0] + self.edgesReduced.remove(out_edges[0]) + + for edge in self.edgesReduced: + self.edgesReducedIndices[edge] = self.allEdges.index(edge) + + @staticmethod + def getEdges(nodes): + """ + Arguments: + nodes: + Nodes of a path in the directed acyclic graph. + + Returns: + List of edges that lie along the path. + """ + return zip(nodes[:-1], nodes[1:]) + + +def writeDagToDotFile(dag, location, dagName="", edgesToLabels=None, + highlightedEdges=None, highlightColor="red"): + """Writes the directed acyclic graph provided to a file in DOT format. + + Arguments: + location: + Location of the file. + dagName: + Name of the directed acyclic graph, as will be written to + the file. If this argument is not provided, the directed + acyclic graph will not have a name. + edgesToLabels: + Dictionary that maps an edge to the label that will annotate + the edge when the DOT file is processed by a visualization tool. + If this argument is not provided, these annotations will not + be made. + highlightedEdges: + List of edges that will be highlighted when the DOT file is + processed by a visualization tool. If this argument + is not provided, no edges will be highlighted. + highlightColor: + Color of the highlighted edges. This argument can be any value + that is legal in the DOT format. If the `highlightedEdges` argument + is not provided, this argument is ignored. + """ + _, extension = os.path.splitext(location) + if extension.lower() != ".dot": + location = "%s.dot" % location + + dagName = " %s" % dagName.strip() + contents = [] + contents.append("digraph%s {" % dagName) + for edge in dag.edges(): + line = " %s -> %s" % edge + attributes = [] + if edgesToLabels: + attributes.append("label = \"%s\"" % edgesToLabels[edge]) + if highlightedEdges and edge in highlightedEdges: + attributes.append("color = \"%s\"" % highlightColor) + if len(attributes) > 0: + line += " [%s]" % ", ".join(attributes) + contents.append("%s;" % line) + contents.append("}") + + try: + dagDotFileHandler = open(location, "w") + except EnvironmentError as e: + errMsg = ("Error writing the DAG to a file located at %s: %s" % + (location, e)) + raise GameTimeError(errMsg) + else: + with dagDotFileHandler: + dagDotFileHandler.write("\n".join(contents)) + +def constructDag(location): + """Constructs a :class:`~gametime.nxHelper.Dag` object to represent + the directed acyclic graph described in DOT format in the file provided. + + Arguments: + location: + Path to the file describing a directed acyclic graph + in DOT format. + + Returns: + :class:`~gametime.nxHelper.Dag` object that represents + the directed acyclic graph. + """ + try: + dagDotFileHandler = open(location, "r") + except EnvironmentError as e: + errMsg = ("Error opening the DOT file, located at %s, that contains " + "the directed acyclic graph to analyze: %s") % (location, e) + raise GameTimeError(errMsg) + else: + with dagDotFileHandler: + dagDotFileLines = dagDotFileHandler.readlines() + + # This is a hacky way of parsing the file, but for this + # small and constant a use case, we should be fine: we should not + # have to generate a parser from a grammar. If, however, for some + # reason, the format with which the Phoenix and Python code communicate + # changes, this should be modified or made more robust. + dagDotFileLines = dagDotFileLines[1:-1] + dagDotFileLines = [line.replace(lsep, "") for line in dagDotFileLines] + dagDotFileLines = [line.replace(";", "") for line in dagDotFileLines] + dagDotFileLines = [line.replace("->", "") for line in dagDotFileLines] + dagDotFileLines = [line.strip() for line in dagDotFileLines] + + # Construct the graph. + dag = Dag() + for line in dagDotFileLines: + edge = line.split(" ") + edge = [node for node in edge if node != ""] + dag.add_edge(edge[0], edge[1]) + dag.loadVariables() + return dag + +def numPaths(dag, source, sink): + """ + Arguments: + dag: + DAG represented by a :class:`~gametime.nxHelper.Dag` object. + source: + Source node. + sink: + Sink node. + + Returns: + Number of paths in the DAG provided. + """ + # Dictionary that maps each node to the number of paths in the + # DAG provided from the input source node to that node. + nodesToNumPaths = {} + nodesToNumPaths[source] = 1 + + # Topologically sort the nodes in the graph. + nodesToVisit = nx.topological_sort(dag) + if nodesToVisit.pop(0) != source: + errMsg = ("The source node should be the first node in " + "a topological sort of the control-flow graph.") + raise GameTimeError(errMsg) + + while len(nodesToVisit) > 0: + currNode = nodesToVisit.pop(0) + numPathsToNode = 0 + for inEdge in dag.in_edges(currNode): + inNeighbor = inEdge[0] + numPathsToNode += nodesToNumPaths[inNeighbor] + nodesToNumPaths[currNode] = numPathsToNode + + return nodesToNumPaths[sink] + +def getRandomPath(dag, source, sink): + """ + Arguments: + dag: + DAG represented by a :class:`~gametime.nxHelper.Dag` object. + source: + Source node. + sink: + Sink node. + + Returns: + Random path in the DAG provided from the input source node to + the input sink node, represented as a list of nodes arranged in + order of traversal from source to sink. + """ + resultPath = [source] + + currNode = source + while currNode != sink: + currNodeNeighbors = dag.neighbors(currNode) + numNeighbors = len(currNodeNeighbors) + if numNeighbors == 1: + neighbor = currNodeNeighbors[0] + resultPath.append(neighbor) + currNode = neighbor + elif numNeighbors > 1: + randPos = randrange(numNeighbors) + randNeighbor = currNodeNeighbors[randPos] + resultPath.append(randNeighbor) + currNode = randNeighbor + else: + # Restart. + resultPath, currNode = [], source + return resultPath + +def hasCycles(dag): + """ + Arguments: + dag: + DAG represented by a :class:`~gametime.nxHelper.Dag` object. + source: + Source node. + sink: + Sink node. + + Returns: + `True` if, and only if, the DAG provided has cycles. + """ + return len(list(nx.simple_cycles(dag))) > 0 diff --git a/src/nx_helper.py b/src/nx_helper.py deleted file mode 100644 index 02637d81..00000000 --- a/src/nx_helper.py +++ /dev/null @@ -1,453 +0,0 @@ -#!/usr/bin/env python - -"""Exposes classes and functions to supplement those provided by -the NetworkX graph package. -""" -from typing import List, Dict, Tuple - -"""See the LICENSE file, located in the root directory of -the source distribution and -at http://verifun.eecs.berkeley.edu/gametime/about/LICENSE, -for details on the GameTime license and authors. -""" - -import os - -from random import randrange - -import networkx as nx - -from defaults import logger -from gametime_error import GameTimeError - -def find_root_node(G): - """ - Parameters: - G : - The graph to find root node in - - Returns: - Node - Root node of G or None if one doesn't exist. - """ - for node in G.nodes(): - if len(list(G.predecessors(node))) == 0: - return node - return None - -def remove_back_edges_to_make_dag(G, root): - """ - Remove all back edges from G to make it a DAG. Assuming G is connected and rooted at ROOT. - - Parameters: - G : - The graph to remove root edges - root : - The root node of G to start DFS with - - Returns: - DAG version of G with all back edges removed. - """ - visited = {node: False for node in G.nodes()} - back_edges = [] - start_node = root - # Iteratively perform DFS on unvisited nodes - stack = [(start_node, iter(G.neighbors(start_node)))] - visited[start_node] = True - - while stack: - parent, children = stack[-1] - try: - child = next(children) - if not visited[child]: - visited[child] = True - stack.append((child, iter(G.neighbors(child)))) - elif child in [node for node, _ in stack]: - # If child is in stack, it's an ancestor, and (parent, child) is a back edge - back_edges.append((parent, child)) - except StopIteration: - stack.pop() - - # Remove identified back edges - G.remove_edges_from(back_edges) - return G - - -class Dag(nx.DiGraph): - """ - Maintains information about the directed acyclic graph (DAG) - of the code being analyzed. It is a subclass of - the `~networkx.DiGraph` class of the NetworkX graph package, - and stores additional information relevant to the GameTime analysis, - such as the special 'default' edges in the DAG. - """ - - def __init__(self, *args, **kwargs): - super(Dag, self).__init__(*args, **kwargs) - - #: Source of the DAG. - - self.source: str = "" - - #: Sink of the DAG. - self.sink: str = "" - - #: Number of nodes in the DAG. - self.num_nodes: int = 0 - - #: Number of edges in the DAG. - self.num_edges: int = 0 - - #: Number of paths in the DAG. - self.num_paths: int = 0 - - #: List of nodes in the DAG. - self.all_nodes: List[str] = [] - self.all_nodes_with_description: List[Tuple[str, Dict[str, str]]] = [] - - #: Dictionary that maps nodes to their indices in the list of all_temp_files nodes. - #: This is maintained for efficiency purposes. - self.nodes_indices: Dict[str, int] = {} - - #: List of nodes in the DAG that are not the sink. - #: We assume that there is only one sink. - #: This is maintained for efficiency purposes. - self.nodes_except_sink: List[str] = [] - - #: Dictionary that maps nodes (except the sink) to their indices in - #: the list of all_temp_files nodes (except the sink). This is maintained for - #: efficiency purposes. - self.nodes_except_sink_indices: Dict[str, int] = {} - - #: List of nodes in the DAG that are neither the source nor the sink. - #: We assume that there is only one source and one sink. This is - #: maintained for efficiency purposes. - self.nodes_except_source_sink: List[str] = [] - - #: Dictionary that maps nodes (except the source and the sink) to their - #: indices in the list of all_temp_files nodes (except the source and the sink). - #: This is maintained for efficiency purposes. - self.nodes_except_source_sink_indices: Dict[str, int] = {} - - #: List of edges in the DAG. - self.all_edges: List[Tuple[str, str]] = [] - - #: Dictionary that maps edges to their indices in the list of all_temp_files edges. - #: This is maintained for efficiency purposes. - self.edges_indices: Dict[Tuple[str, str]: int] = {} - - #: List of non-special edges in the DAG. - self.edges_reduced: List[Tuple[str, str]] = [] - - #: Dictionary that maps non-special edges to their indices in the - #: list of all_temp_files edges. This is maintained for efficiency purposes. - self.edges_reduced_indices: Dict[Tuple[str, str], int] = {} - - #: Dictionary that maps nodes to the special ('default') edges. - self.special_edges: Dict[str, Tuple[str, str]] = {} - - #: List of the weights assigned to the edges in the DAG, arranged - #: in the same order as the edges are in the list of all_temp_files edges. - self.edge_weights: List[int] = [] - - def initialize_dictionaries(self): - """ """ - self.num_nodes = self.number_of_nodes() - self.num_edges = self.number_of_edges() - self.all_nodes = nodes = sorted(self.nodes()) - self.all_nodes_with_description = sorted(self.nodes.data(), key=lambda x: x[0]) - self.all_edges = self.edges() - - # We assume there is only one source and one sink. - self.source = [node for node in nodes if (self.in_degree(node) == 0)][0] - self.sink = [node for node in nodes if (self.out_degree(node) == 0)][0] - self.nodes_except_sink = [node for node in self.all_nodes - if node != self.sink] - self.nodes_except_source_sink = [node for node in self.all_nodes - if (node != self.source and node != self.sink)] - - self.num_paths = (0 if has_cycles(self) else - num_paths(self, self.source, self.sink)) - - # Initialize dictionaries that map nodes and edges to their indices - # in the node list and edge list, respectively. - for node_index, node in enumerate(self.all_nodes): - self.nodes_indices[node] = node_index - for node_index, node in enumerate(self.nodes_except_sink): - self.nodes_except_sink_indices[node] = node_index - for node_index, node in enumerate(self.nodes_except_source_sink): - self.nodes_except_source_sink_indices[node] = node_index - for edge_index, edge in enumerate(self.all_edges): - self.edges_indices[edge] = edge_index - - def load_variables(self): - """ - Loads the instance variables of this object with appropriate - values. This method is useful when the DAG is loaded from a DOT file. - - """ - self.initialize_dictionaries() - self.reset_edge_weights() - - logger.info("Initializing data structures for special edges...") - self._init_special_edges() - logger.info("Data structures initialized.") - - def reset_edge_weights(self): - """Resets the weights assigned to the edges of the DAG.""" - self.edge_weights = [0] * self.num_edges - - def _init_special_edges(self): - """ - To reduce the dimensionality to b = n-m+2, each node, except for - the source and sink, chooses a 'special' edge. This edge is taken if - flow enters the node, but no outgoing edge is 'visibly' selected. - In other words, it is the 'default' edge for the node. - - This method initializes all_temp_files of the data structures necessary - to keep track of these special edges. - - """ - self.edges_reduced = list(self.all_edges) - - for node in self.nodes_except_source_sink: - out_edges = self.out_edges(node) - out_edges = list(out_edges) - if len(out_edges) > 0: - # By default, we pick the first edge as 'special'. - self.special_edges[node] = out_edges[0] - self.edges_reduced.remove(out_edges[0]) - - for edge in self.edges_reduced: - self.edges_reduced_indices[edge] = list(self.all_edges).index(edge) - - @staticmethod - def get_edges(nodes: List[str]) -> List[Tuple[str, str]]: - """ - Parameters: - nodes: List[str] : - Nodes of a path in the directed acyclic graph - Returns: - List of edges that lie along the path. - - """ - return list(zip(nodes[:-1], nodes[1:])) - - def get_node_label(self, node: int) -> str: - """gets node label from node ID - - Parameters: - node: int : - ID of the node of interest - - Returns: - label corresponding to the node. (code corresponding to the node in LLVM IR) - - """ - return self.all_nodes_with_description[node][1]["label"] - - -def write_dag_to_dot_file(dag: Dag, location: str, dag_name: str = "", - edges_to_labels: Dict[Tuple[str, str], str] = None, - highlighted_edges: List[Tuple[str, str]] = None, - highlight_color: str = "red"): - """ - Writes the directed acyclic graph provided to a file in DOT format. - - Parameters: - dag : Dag : - Dag to save to dot - location : str : - Location of the file. - dag_name : str : - Name of the directed acyclic graph, as will be written to - the file. If this argument is not provided, the directed - acyclic graph will not have a name. (Default value = "") - edges_to_labels : Dict[Tuple[str, str], str]: - Dictionary that maps an edge to the label that will annotate - the edge when the DOT file is processed by a visualization tool. - If this argument is not provided, these annotations will not - be made. (Default value = None) - highlighted_edges : List[Tuple[str, str]]: - List of edges that will be highlighted when the DOT file is - processed by a visualization tool. If this argument - is not provided, no edges will be highlighted. (Default value = None) - highlight_color : str: - Color of the highlighted edges. This argument can be any value - that is legal in the DOT format. If the `highlightedEdges` argument - is not provided, this argument is ignored. (Default value = "red") - - """ - _, extension = os.path.splitext(location) - if extension.lower() != ".dot": - location = "%s.dot" % location - - dag_name = " %s" % dag_name.strip() - contents = ["digraph%s {" % dag_name] - - for node in dag.all_nodes_with_description: - line: str = " %s" % node[0] - attributes: List[str] = [] - for key in node[1]: - attributes.append(' %s="%s"' % (key, node[1][key])) - line += " [%s]" % ", ".join(attributes) - contents.append("%s;" % line) - - for edge in dag.edges(): - line = " %s -> %s" % edge - attributes = [] - if edges_to_labels: - attributes.append("label = \"%s\"" % edges_to_labels[edge]) - if highlighted_edges and edge in highlighted_edges: - attributes.append("color = \"%s\"" % highlight_color) - if len(attributes) > 0: - line += " [%s]" % ", ".join(attributes) - contents.append("%s;" % line) - contents.append("}") - - try: - dag_dot_file_handler = open(location, "w") - except EnvironmentError as e: - err_msg = ("Error writing the DAG to a file located at %s: %s" % - (location, e)) - raise GameTimeError(err_msg) - else: - with dag_dot_file_handler: - dag_dot_file_handler.write("\n".join(contents)) - - -def construct_dag(location: str) -> tuple[Dag, bool]: - """ - Constructs a `~gametime.nxHelper.Dag` object to represent - the directed acyclic graph described in DOT format in the file provided. - - Parameters: - location: str : - Path to the file describing a directed acyclic graph in DOT format - - Returns: - `~gametime.src.nx_helper.Dag`: - Object that represents the directed acyclic graph. - - """ - try: - with open(location, "r") as f: - graph_from_dot: nx.Graph = nx.nx_agraph.read_dot(f) - - except EnvironmentError as e: - err_msg: str = ("Error opening the DOT file, located at %s, that contains " - "the directed acyclic graph to analyze: %s") % (location, e) - raise GameTimeError(err_msg) - - if not graph_from_dot.is_directed(): - raise GameTimeError("CFG isn't directed") - - root = find_root_node(graph_from_dot) - if root is None: - raise GameTimeError("There is no node without incoming edge in CFG.") - - sink_nodes = [node for node, out_degree in graph_from_dot.out_degree() if out_degree == 0] - if len(sink_nodes) != 1: - raise GameTimeError("The number of sink nodes don't equal to 1.") - - modifed=False - if len(list(nx.simple_cycles(graph_from_dot))) > 0: - logger.warning("The control-flow graph has cycles. Trying to remove them by removing back edges.") - graph_from_dot = remove_back_edges_to_make_dag(graph_from_dot, root) - modifed = True - - dag: Dag = Dag(graph_from_dot) - dag.load_variables() - return dag, modifed - - -def num_paths(dag: Dag, source: str, sink: str) -> int: - """ - - Parameters: - dag: - DAG represented by a `~gametime.src.nx_helper.Dag` object. - source: - Source node. - sink: - Sink node. - Returns: - int: - Number of paths in the DAG provided. Note: Passed in DAG must be actually acyclic. - - """ - - if has_cycles(dag): - err_msg = ("The dag has cycles, so number of path is infinite. Get rid of cycles before analyzing") - raise GameTimeError(err_msg) - # Dictionary that maps each node to the number of paths in the - # DAG provided from the input source node to that node. - nodes_to_num_paths: Dict[str, int] = {source: 1} - - # Topologically sort the nodes in the graph. - nodes_to_visit: List[str] = list(nx.topological_sort(dag)) - if nodes_to_visit.pop(0) != source: - err_msg = ("The source node should be the first node in " - "a topological sort of the control-flow graph.") - raise GameTimeError(err_msg) - - while len(nodes_to_visit) > 0: - curr_node = nodes_to_visit.pop(0) - num_paths_to_node = 0 - for inEdge in dag.in_edges(curr_node): - in_neighbor = inEdge[0] - num_paths_to_node += nodes_to_num_paths[in_neighbor] - nodes_to_num_paths[curr_node] = num_paths_to_node - - return nodes_to_num_paths[sink] - - -def get_random_path(dag: Dag, source: str, sink: str) -> List[str]: - """ - Parameters: - dag: Dag : - DAG represented by a `~gametime.src.nx_helper.Dag` object. - source: str : - source to start path with - sink: str : - sink to end path with - Returns: - List[str]: - Random path in the DAG provided from the input source node to - the input sink node, represented as a list of nodes arranged in - order of traversal from source to sink. - - """ - result_path = [source] - - curr_node: str = source - while curr_node != sink: - curr_node_neighbors: List[str] = list(dag.neighbors(curr_node)) - num_neighbors: int = len(curr_node_neighbors) - if num_neighbors == 1: - neighbor: str = curr_node_neighbors[0] - result_path.append(neighbor) - curr_node = neighbor - elif num_neighbors > 1: - rand_pos: int = randrange(num_neighbors) - rand_neighbor: str = curr_node_neighbors[rand_pos] - result_path.append(rand_neighbor) - curr_node = rand_neighbor - else: - # Restart. - result_path, curr_node = [], source - return result_path - - -def has_cycles(dag: Dag) -> bool: - """ - Parameters: - dag: Dag : - DAG represented by a dag - - Returns: - bool: - `True` if, and only if, the DAG provided has cycles. - - """ - return len(list(nx.simple_cycles(dag))) > 0 diff --git a/src/path.py b/src/path.py index 92a4b843..3f35c331 100644 --- a/src/path.py +++ b/src/path.py @@ -3,7 +3,13 @@ """Defines a class that maintains a representation of, and information about, a single path in the code that is being analyzed. """ -from typing import List, Dict, Tuple + +"""See the LICENSE file, located in the root directory of +the source distribution and +at http://verifun.eecs.berkeley.edu/gametime/about/LICENSE, +for details on the GameTime license and authors. +""" + import os @@ -11,33 +17,64 @@ from pulp import PulpError -from gametime_error import GameTimeError -from index_expression import VariableIndexExpression, ArrayIndexExpression, IndexExpression -from pulp_helper import IlpProblem - -"""See the LICENSE file, located in the root directory of -the source distribution and -at http://verifun.eecs.berkeley.edu/gametime/about/LICENSE, -for details on the GameTime license and authors. -""" +from gametimeError import GameTimeError +from indexExpression import VariableIndexExpression, ArrayIndexExpression class Path(object): - """ - Maintains a representation of, and information about, + """Maintains a representation of, and information about, a single path in the code that is being analyzed. + + Attributes: + ilpProblem: + :class:`~gametime.ilpProblem.IlpProblem` object that represents + the integer linear programming problem that, when solved, + produced this path. + nodes: + IDs of the nodes in a directed acyclic graph along this path, + represented as a list of strings. + lineNumbers: + Line numbers of the source-level statements along this path, + represented as a list of positive integers. + conditions: + Conditions along this path, represented as a list of strings. + conditionEdges: + Dictionary that associates the number of a condition with + the edge in the directed acyclic graph that is associated + with the condition, represented as a tuple. + conditionTruths: + Dictionary that associates the line numbers of the conditional + points in the code being analyzed with their truth values. + arrayAccesses: + Information about array accesses made in conditions along + this path, represented as a dictionary that maps the name of + an array to a list of tuples, each of which contains + the numbers of the temporary index variables in an array access. + aggIndexExprs: + Information about the expressions associated with the temporary + index variables of aggregate accesses along this path, represented + as a dictionary that maps the number of a temporary index variable + to an :class:`~gametime.indexExpression.IndexExpression` object. + smtQuery: + ``Query`` object that represents the SMT query used to determine + the feasibility of this path. + assignments: + Dictionary of assignments to variables that would drive + an execution of the code along this path. + predictedValue: + Predicted value (runtime, energy consumption, etc.) of this path. + measuredValue: + Measured value (runtime, energy consumption, etc.) of this path. """ - def __init__(self, ilp_problem: IlpProblem = None, nodes: List[str] = None, - line_numbers: List[str] = None, - conditions: List[str] = None, condition_edges: Dict[int, Tuple[str]] = None, - condition_truths: Dict[str, bool] = None, - array_accesses: Dict[str, List[Tuple[int]]] = None, - agg_index_exprs: IndexExpression = None, assignments: Dict[str, str] = None, - predicted_value: float = 0, measured_value: float = 0): + def __init__(self, ilpProblem=None, nodes=None, lineNumbers=None, + conditions=None, conditionEdges=None, conditionTruths=None, + arrayAccesses=None, aggIndexExprs=None, + smtQuery=None, assignments=None, + predictedValue=0, measuredValue=0): #: Integer linear programming problem that, when solved, produced #: this path, represented as an ``IlpProblem`` object. - self.ilp_problem = ilp_problem + self.ilpProblem = ilpProblem #: IDs of the nodes in a directed acyclic graph along this path, #: represented as a list of strings. @@ -45,7 +82,7 @@ def __init__(self, ilp_problem: IlpProblem = None, nodes: List[str] = None, #: Line numbers of the source-level statements along this path, #: represented as a list of positive integers. - self.line_numbers = line_numbers or [] + self.lineNumbers = lineNumbers or [] #: Conditions along this path, represented as a list of strings. self.conditions = conditions or [] @@ -53,23 +90,27 @@ def __init__(self, ilp_problem: IlpProblem = None, nodes: List[str] = None, #: Dictionary that associates the number of a condition with #: the edge in the directed acyclic graph that is associated with #: the condition. The edge is represented as a tuple. - self.condition_edges = condition_edges or {} + self.conditionEdges = conditionEdges or {} #: Dictionary that associates the line numbers of the conditional points #: in the code being analyzed with their truth values. - self.condition_truths = condition_truths or {} + self.conditionTruths = conditionTruths or {} #: Information about array accesses made in conditions along this path, #: represented as a dictionary that maps the name of an array to a #: list of tuples, each of which contains the numbers of the temporary #: index variables in an array access. - self.array_accesses = array_accesses or {} + self.arrayAccesses = arrayAccesses or {} #: Information about the expressions associated with the temporary #: index variables of aggregate accesses along this path, represented #: as a dictionary that maps the number of a temporary index variable #: to an ``IndexExpression`` object. - self.agg_index_exprs = agg_index_exprs or {} + self.aggIndexExprs = aggIndexExprs or {} + + #: SMT query that was used to determine the feasibility of this path, + #: represented as a ``Query`` object. + self.smtQuery = smtQuery #: Dictionary of assignments to variables that would drive an execution #: of the code along this path. @@ -78,535 +119,489 @@ def __init__(self, ilp_problem: IlpProblem = None, nodes: List[str] = None, #: Predicted value (runtime, energy consumption, etc.) #: of this path, represented as a number (either #: an integer or a floating-point number). - self.predicted_value = predicted_value + self.predictedValue = predictedValue #: Measured value (runtime, energy consumption, etc.) #: of this path, represented as a number (either #: an integer or a floating-point number). - self.measured_value = measured_value - - self.path_analyzer = None + self.measuredValue = measuredValue - self.name = None - - def write_ilp_problem_to_lp_file(self, location) -> None: + def writeIlpProblemToLpFile(self, location): """ Writes, to an LP file, the integer linear programming problem that, when solved, produced this path. - Parameters: - location : - Location of the file - + Arguments: + location: + Location of the file. """ - if self.ilp_problem is not None: + if self.ilpProblem is not None: _, extension = os.path.splitext(location) if extension.lower() != ".lp": - location += ".lp" + location = location + ".lp" try: - self.ilp_problem.writeLP(location) + self.ilpProblem.writeLP(location) except (PulpError, EnvironmentError) as e: - err_msg = ("Error writing the integer linear programming " - "problem to an LP file: %s") % e - raise GameTimeError(err_msg) + errMsg = ("Error writing the integer linear programming " + "problem to an LP file: %s") % e + raise GameTimeError(errMsg) else: - err_msg = ("This path is not associated with an integer linear " - "programming problem.") - raise GameTimeError(err_msg) + errMsg = ("This path is not associated with an integer linear " + "programming problem.") + raise GameTimeError(errMsg) - def get_nodes(self) -> str: + def getNodes(self): """ Returns: - str: - String representation of the IDs of the nodes in - a directed acyclic graph along this path. - + String representation of the IDs of the nodes in + a directed acyclic graph along this path. """ return " ".join(self.nodes) - def write_nodes_to_file(self, location: str) -> None: + def writeNodesToFile(self, location): """ Writes the IDs of the nodes in a directed acyclic graph along this path to a file. - Parameters: - location: str : - Location of the file - + Arguments: + location: + Location of the file. """ try: - nodes_file_handler = open(location, "w") + nodesFileHandler = open(location, "w") except EnvironmentError as e: - err_msg = ("Error writing the IDs of the nodes in " - "a directed acyclic graph along the path: %s") % e - raise GameTimeError(err_msg) + errMsg = ("Error writing the IDs of the nodes in " + "a directed acyclic graph along the path: %s") % e + raise GameTimeError(errMsg) else: - with nodes_file_handler: - nodes_file_handler.write(self.get_nodes()) + with nodesFileHandler: + nodesFileHandler.write(self.getNodes()) @staticmethod - def read_nodes_from_file(location: str) -> List[str]: + def readNodesFromFile(location): """ Reads the IDs of the nodes in a directed acyclic graph along a path from a file. - Parameters: - location: str : - Location of the file - + Arguments: + location: + Location of the file. Returns: - List[str] - IDs of the nodes in a directed acyclic graph along a path, - represented as a list of strings. - + IDs of the nodes in a directed acyclic graph along a path, + represented as a list of strings. """ try: - nodes_file_handler = open(location, "r") + nodesFileHandler = open(location, "r") except EnvironmentError as e: - err_msg = ("Error reading the IDs of the nodes in " - "a directed acyclic graph along a path: %s") % e - raise GameTimeError(err_msg) + errMsg = ("Error reading the IDs of the nodes in " + "a directed acyclic graph along a path: %s") % e + raise GameTimeError(errMsg) else: - with nodes_file_handler: - nodes = nodes_file_handler.readline().strip().split() + with nodesFileHandler: + nodes = nodesFileHandler.readline().strip().split() return nodes - def get_line_numbers(self) -> str: + def getLineNumbers(self): """ - Returns: - str: - String representation of the line numbers of - the source-level statements that lie along this path. - + String representation of the line numbers of + the source-level statements that lie along this path. """ - return " ".join([str(lineNumber) for lineNumber in self.line_numbers]) + return " ".join([str(lineNumber) for lineNumber in self.lineNumbers]) - def write_line_numbers_to_file(self, location: str) -> None: + def writeLineNumbersToFile(self, location): """ Writes the line numbers of the source-level statements that lie along this path to a file. - Parameters: - location: str: - Location of the file - + Arguments: + location: + Location of the file. """ try: - line_numbers_file_handler = open(location, "w") + lineNumbersFileHandler = open(location, "w") except EnvironmentError as e: - err_msg = ("Error writing line numbers of the source-level " - "statements along the path: %s") % e - raise GameTimeError(err_msg) + errMsg = ("Error writing line numbers of the source-level " + "statements along the path: %s") % e + raise GameTimeError(errMsg) else: - with line_numbers_file_handler: - line_numbers_file_handler.write(self.get_line_numbers()) + with lineNumbersFileHandler: + lineNumbersFileHandler.write(self.getLineNumbers()) @staticmethod - def read_line_numbers_from_file(location: str) -> List[int]: + def readLineNumbersFromFile(location): """ Reads the line numbers of the source-level statements that lie along a path from a file. - Parameters: - location: str : - Location of the file + Arguments: + location: + Location of the file. Returns: - List[int] - Line numbers of the source-level statements along this path, - represented as a list of positive integers. - + Line numbers of the source-level statements along this path, + represented as a list of positive integers. """ try: - line_numbers_file_handler = open(location, "r") + lineNumbersFileHandler = open(location, "r") except EnvironmentError as e: - err_msg = ("Error reading line numbers of the source-level " - "statements along a path: %s") % e - raise GameTimeError(err_msg) + errMsg = ("Error reading line numbers of the source-level " + "statements along a path: %s") % e + raise GameTimeError(errMsg) else: - with line_numbers_file_handler: - line_numbers = line_numbers_file_handler.readline().strip().split() - line_numbers = [int(lineNumber) for lineNumber in line_numbers] - return line_numbers + with lineNumbersFileHandler: + lineNumbers = lineNumbersFileHandler.readline().strip().split() + lineNumbers = [int(lineNumber) for lineNumber in lineNumbers] + return lineNumbers - def get_conditions(self) -> str: + def getConditions(self): """ Returns: - str: - String representation of the conditions along this path. - + String representation of the conditions along this path. """ return "\n".join(self.conditions) - def write_conditions_to_file(self, location: str) -> None: + def writeConditionsToFile(self, location): """ Writes the conditions along this path to a file. - Parameters: - location: str : - Location of the file - + Arguments: + location: + Location of the file. """ try: - conditions_file_handler = open(location, "w") + conditionsFileHandler = open(location, "w") except EnvironmentError as e: - err_msg = "Error writing conditions along the path: %s" % e - raise GameTimeError(err_msg) + errMsg = "Error writing conditions along the path: %s" % e + raise GameTimeError(errMsg) else: - with conditions_file_handler: - conditions_file_handler.write(self.get_conditions()) + with conditionsFileHandler: + conditionsFileHandler.write(self.getConditions()) @staticmethod - def read_conditions_from_file(location: str) -> List[str]: + def readConditionsFromFile(location): """ Reads the conditions along a path from a file. - Parameters: - location: str : - Location of the file + Arguments: + location: + Location of the file. Returns: - List[str]: - Conditions along a path, represented as a list of strings. - + Conditions along a path, represented as a list of strings. """ try: - conditions_file_handler = open(location, "r") + conditionsFileHandler = open(location, "r") except EnvironmentError as e: - err_msg = "Error reading conditions along a path: %s" % e - raise GameTimeError(err_msg) + errMsg = "Error reading conditions along a path: %s" % e + raise GameTimeError(errMsg) else: - with conditions_file_handler: - conditions = conditions_file_handler.readlines() + with conditionsFileHandler: + conditions = conditionsFileHandler.readlines() conditions = [condition.strip() for condition in conditions] conditions = [condition for condition in conditions - if condition != ""] + if condition is not ""] return conditions - def get_condition_edges(self) -> str: + def getConditionEdges(self): """ Returns: - str: - String representation of the numbers of the conditions along - this path, and the edges that are associated with the conditions. - + String representation of the numbers of the conditions along + this path, and the edges that are associated with the conditions. """ result = [] - sorted_keys = sorted(self.condition_edges.keys()) - for key in sorted_keys: + sortedKeys = sorted(self.conditionEdges.keys()) + for key in sortedKeys: result.append("%s: " % key) - result.append(" ".join(self.condition_edges[key])) + result.append(" ".join(self.conditionEdges[key])) result.append("\n") return "".join(result) - def write_condition_edges_to_file(self, location: str) -> None: + def writeConditionEdgesToFile(self, location): """ Writes the numbers of the conditions along this path, and the edges that are associated with the conditions, to a file. - Parameters: - location: str : - Location of the file - + Arguments: + location: + Location of the file. """ try: - condition_edges_file_handler = open(location, "w") + conditionEdgesFileHandler = open(location, "w") except EnvironmentError as e: - err_msg = ("Error writing the numbers of the conditions along " - "the path, and the edges that are associated with " - "the conditions: %s") % e - raise GameTimeError(err_msg) + errMsg = ("Error writing the numbers of the conditions along " + "the path, and the edges that are associated with " + "the conditions: %s") % e + raise GameTimeError(errMsg) else: - with condition_edges_file_handler: - condition_edges_file_handler.write(self.get_condition_edges()) + with conditionEdgesFileHandler: + conditionEdgesFileHandler.write(self.getConditionEdges()) @staticmethod - def read_condition_edges_from_file(location: str) -> Dict[int, Tuple[str]]: + def readConditionEdgesFromFile(location): """ Reads the numbers of the conditions along a path, and the edges that are associated with the conditions, from a file. - Parameters: - location: str : - Location of the file + Arguments: + location: + Location of the file. Returns: - Dict[int, Tuple[str]]: - Dictionary that associates the number of a condition with - the edge in the directed acyclic graph that is associated with - the condition. The edge is represented as a tuple. - + Dictionary that associates the number of a condition with + the edge in the directed acyclic graph that is associated with + the condition. The edge is represented as a tuple. """ try: - condition_edges_file_handler = open(location, "r") + conditionEdgesFileHandler = open(location, "r") except EnvironmentError as e: - err_msg = ("Error reading the numbers of the conditions along " - "a path, and the edges that are associated with " - "the conditions: %s") % e - raise GameTimeError(err_msg) + errMsg = ("Error reading the numbers of the conditions along " + "a path, and the edges that are associated with " + "the conditions: %s") % e + raise GameTimeError(errMsg) else: - condition_edges = {} - with condition_edges_file_handler: - condition_edges_file_lines = condition_edges_file_handler.readlines() - for line in condition_edges_file_lines: + conditionEdges = {} + with conditionEdgesFileHandler: + conditionEdgesFileLines = conditionEdgesFileHandler.readlines() + for line in conditionEdgesFileLines: (conditionNum, edge) = line.strip().split(": ") edge = tuple(edge.strip().split(" ")) - condition_edges[int(conditionNum.strip())] = edge - return condition_edges + conditionEdges[int(conditionNum.strip())] = edge + return conditionEdges - def get_edges_for_conditions(self, condition_nums: List[int]) -> List[Tuple[str]]: + def getEdgesForConditions(self, conditionNums): """ - - Parameters: - condition_nums: List[int] : - List of conditions + Arguments: + conditionNums: + List of (non-negative) numbers of conditions along this path. Returns: - List[Tuple[str]]: - List of the edges that are associated with the conditions - (along this path) whose numbers are provided. Each edge is - represented as a tuple, and appears only once in the list. - + List of the edges that are associated with the conditions + (along this path) whose numbers are provided. Each edge is + represented as a tuple, and appears only once in the list. """ - return list(set([self.condition_edges[conditionNum] - for conditionNum in condition_nums])) + return list(set([self.conditionEdges[conditionNum] + for conditionNum in conditionNums])) - def get_condition_truths(self) -> str: + def getConditionTruths(self): """ Returns: - str: - String representation of the line numbers of the conditional points - in the code being analyzed, along with their truth values. - + String representation of the line numbers of the conditional points + in the code being analyzed, along with their truth values. """ result = [] - sorted_keys = sorted([int(key) for key in self.condition_truths.keys()]) - for key in sorted_keys: + sortedKeys = sorted([int(key) for key in self.conditionTruths.keys()]) + for key in sortedKeys: result.append("%s: " % key) - result.append(str(self.condition_truths[str(key)])) + result.append(str(self.conditionTruths[str(key)])) result.append("\n") return "".join(result) - def write_condition_truths_to_file(self, location: str) -> None: + def writeConditionTruthsToFile(self, location): """ Writes the line numbers of the conditional points in the code being analyzed, along with their truth values, to a file. - Parameters: - location: str : - Location of the file + Arguments: + location: + Location of the file. """ try: - condition_truths_file_handler = open(location, "w") + conditionTruthsFileHandler = open(location, "w") except EnvironmentError as e: - err_msg = ("Error writing line numbers of the conditional " - "points in the code being analyzed, along with " - "their truth values: %s") % e - raise GameTimeError(err_msg) + errMsg = ("Error writing line numbers of the conditional " + "points in the code being analyzed, along with " + "their truth values: %s") % e + raise GameTimeError(errMsg) else: - with condition_truths_file_handler: - condition_truths_file_handler.write(self.get_condition_truths()) + with conditionTruthsFileHandler: + conditionTruthsFileHandler.write(self.getConditionTruths()) @staticmethod - def read_condition_truths_from_file(location: str) -> Dict[int, bool]: + def readConditionTruthsFromFile(location): """ Reads the line numbers of the conditional points in the code being analyzed, along with their truth values along a path, from a file. - Parameters: - location: str : - Location of the file - - Returns: - Dict[int, bool]: - Dictionary that associates the line numbers of - the conditional points in the code being analyzed with - their truth values. + Arguments: + location: + Location of the file. + Returns: + Dictionary that associates the line numbers of + the conditional points in the code being analyzed with + their truth values. """ try: - condition_truths_file_handler = open(location, "r") + conditionTruthsFileHandler = open(location, "r") except EnvironmentError as e: - err_msg = ("Error reading line numbers of the conditional " - "points in the code being analyzed, along with " - "their truth values along a path: %s") % e - raise GameTimeError(err_msg) + errMsg = ("Error reading line numbers of the conditional " + "points in the code being analyzed, along with " + "their truth values along a path: %s") % e + raise GameTimeError(errMsg) else: - condition_truths = {} - with condition_truths_file_handler: - condition_truths_file_lines = \ - condition_truths_file_handler.readlines() - for line in condition_truths_file_lines: + conditionTruths = {} + with conditionTruthsFileHandler: + conditionTruthsFileLines = \ + conditionTruthsFileHandler.readlines() + for line in conditionTruthsFileLines: (lineNumber, conditionTruth) = line.strip().split(": ") - condition_truths[lineNumber] = conditionTruth == "True" - return condition_truths + conditionTruths[lineNumber] = conditionTruth == "True" + return conditionTruths - def get_array_accesses(self) -> str: + def getArrayAccesses(self): """ Returns: - str: - String representation of the array accesses made in - conditions along this path. - + String representation of the array accesses made in + conditions along this path. """ result = [] - for array_name in self.array_accesses: - result.append("%s: " % array_name) - result.append(str(self.array_accesses[array_name])) + for arrayName in self.arrayAccesses: + result.append("%s: " % arrayName) + result.append(str(self.arrayAccesses[arrayName])) result.append("\n") return "".join(result) - def write_array_accesses_to_file(self, location: str) -> None: + def writeArrayAccessesToFile(self, location): """ Writes information about the array accesses made in conditions along this path to a file. - Parameters: - location: str : - Location of the file + Arguments: + location: + Location of the file. """ try: - array_accesses_file_handler = open(location, "w") + arrayAccessesFileHandler = open(location, "w") except EnvironmentError as e: - err_msg = ("Error writing information about the array accesses " - "made in conditions along the path: %s") % e - raise GameTimeError(err_msg) + errMsg = ("Error writing information about the array accesses " + "made in conditions along the path: %s") % e + raise GameTimeError(errMsg) else: - with array_accesses_file_handler: - array_accesses_file_handler.write(self.get_array_accesses()) + with arrayAccessesFileHandler: + arrayAccessesFileHandler.write(self.getArrayAccesses()) @staticmethod - def read_array_accesses_from_file(location: str) -> Dict[str, List[Tuple[int, int]]]: + def readArrayAccessesFromFile(location): """ Reads information about the array accesses made in conditions along a path from a file. - Parameters: - location: str : - Location of the file - + Arguments: + location: + Location of the file. Returns: - Dict[str, List[Tuple[int, int]]]: - Dictionary that maps an array name to a list of tuples, each of - which contains the numbers of the temporary index variables - in an array access. - + Dictionary that maps an array name to a list of tuples, each of + which contains the numbers of the temporary index variables + in an array access. """ try: - array_accesses_file_handler = open(location, "r") + arrayAccessesFileHandler = open(location, "r") except EnvironmentError as e: - err_msg = ("Error reading information about the array accesses " - "made in conditions along a path: %s") % e - raise GameTimeError(err_msg) + errMsg = ("Error reading information about the array accesses " + "made in conditions along a path: %s") % e + raise GameTimeError(errMsg) else: - array_accesses = {} - with array_accesses_file_handler: - array_access_lines = array_accesses_file_handler.readlines() - for array_access_line in array_access_lines: + arrayAccesses = {} + with arrayAccessesFileHandler: + arrayAccessLines = arrayAccessesFileHandler.readlines() + for arrayAccessLine in arrayAccessLines: # Process the array access information. - array_access_line = array_access_line.strip().split(": ") - array_name = array_access_line[0] + arrayAccessLine = arrayAccessLine.strip().split(": ") + arrayName = arrayAccessLine[0] - temp_indices_tuples = literal_eval(array_access_line[1]) - if array_name in array_accesses: - array_accesses[array_name].extend(temp_indices_tuples) + tempIndicesTuples = literal_eval(arrayAccessLine[1]) + if arrayName in arrayAccesses: + arrayAccesses[arrayName].extend(tempIndicesTuples) else: - array_accesses[array_name] = temp_indices_tuples - return array_accesses + arrayAccesses[arrayName] = tempIndicesTuples + return arrayAccesses - def get_agg_index_exprs(self) -> str: + def getAggIndexExprs(self): """ Returns: - str: - String representation of the expressions associated - with the temporary index variables of aggregate accesses - along a path. - + String representation of the expressions associated + with the temporary index variables of aggregate accesses + along a path. """ result = [] - for index_number in self.agg_index_exprs: - result.append("%d: " % index_number) - result.append(str(self.agg_index_exprs[index_number])) + for indexNumber in self.aggIndexExprs: + result.append("%d: " % indexNumber) + result.append(str(self.aggIndexExprs[indexNumber])) result.append("\n") return "".join(result) - def write_agg_index_exprs_to_file(self, location: str) -> None: + def writeAggIndexExprsToFile(self, location): """ Writes information about the expressions associated with the temporary index variables of aggregate accesses along this path to a file. - Parameters: - location: str : - Location of the file + Arguments: + location: + Location of the file. """ try: - agg_index_exprs_file_handler = open(location, "w") + aggIndexExprsFileHandler = open(location, "w") except EnvironmentError as e: - err_msg = ("Error writing information about the expressions " - "associated with the temporary index variables of " - "aggregate accesses along this path: %s") % e - raise GameTimeError(err_msg) + errMsg = ("Error writing information about the expressions " + "associated with the temporary index variables of " + "aggregate accesses along this path: %s") % e + raise GameTimeError(errMsg) else: - with agg_index_exprs_file_handler: - agg_index_exprs_file_handler.write(self.get_agg_index_exprs()) + with aggIndexExprsFileHandler: + aggIndexExprsFileHandler.write(self.getAggIndexExprs()) @staticmethod - def read_agg_index_exprs_from_file(location: str) -> Dict[int, IndexExpression]: + def readAggIndexExprsFromFile(location): """ Reads, from a file, information about the expressions associated with the temporary index variables of aggregate accesses along a path. - Parameters: - location: str : - Location of the file - + Arguments: + location: + Location of the file. Returns: - Dict[int, IndexExpression]: - Dictionary that maps the number of a temporary index - variable to an ``IndexExpression`` object. - + Dictionary that maps the number of a temporary index + variable to an ``IndexExpression`` object. """ try: - agg_index_exprs_file_handler = open(location, "r") + aggIndexExprsFileHandler = open(location, "r") except EnvironmentError as e: - err_msg = ("Error reading information about the expressions " - "associated with the temporary index variables of " - "aggregate accesses along a path: %s") % e - raise GameTimeError(err_msg) + errMsg = ("Error reading information about the expressions " + "associated with the temporary index variables of " + "aggregate accesses along a path: %s") % e + raise GameTimeError(errMsg) else: - agg_index_exprs = {} - with agg_index_exprs_file_handler: - agg_index_exprs_lines = agg_index_exprs_file_handler.readlines() - for aggIndexExprsLine in agg_index_exprs_lines: - line_tokens = aggIndexExprsLine.strip().split(": ") - - temp_index_number = int(line_tokens[0]) - line_tokens = line_tokens[1].split() - if len(line_tokens) == 1: - var_name = line_tokens[0] - agg_index_exprs[temp_index_number] = \ - VariableIndexExpression(var_name) + aggIndexExprs = {} + with aggIndexExprsFileHandler: + aggIndexExprsLines = aggIndexExprsFileHandler.readlines() + for aggIndexExprsLine in aggIndexExprsLines: + lineTokens = aggIndexExprsLine.strip().split(": ") + + tempIndexNumber = int(lineTokens[0]) + lineTokens = lineTokens[1].split() + if len(lineTokens) == 1: + varName = lineTokens[0] + aggIndexExprs[tempIndexNumber] = \ + VariableIndexExpression(varName) else: - array_var_name = line_tokens[0] - indices = tuple(int(index) for index in line_tokens[1:]) - agg_index_exprs[temp_index_number] = \ - ArrayIndexExpression(array_var_name, indices) - return agg_index_exprs + arrayVarName = lineTokens[0] + indices = tuple(int(index) for index in lineTokens[1:]) + aggIndexExprs[tempIndexNumber] = \ + ArrayIndexExpression(arrayVarName, indices) + return aggIndexExprs - def get_assignments(self) -> str: + def getAssignments(self): """ Returns: - str: - String representation of the assignments to variables - that would drive an execution of the code under analysis - along this path. - + String representation of the assignments to variables + that would drive an execution of the code under analysis + along this path. """ result = [] for key in sorted(self.assignments.keys()): @@ -614,230 +609,214 @@ def get_assignments(self) -> str: result.append("\n") return "".join(result) - def write_assignments_to_file(self, location: str) -> None: + def writeAssignmentsToFile(self, location): """ Writes the assignments to variables that would drive an execution of the code under analysis along this path to a file. - Parameters: - location: str : - Location of the file + Arguments: + location: + Location of the file. """ try: - assignment_file_handler = open(location, "w") + assignmentFileHandler = open(location, "w") except EnvironmentError as e: - err_msg = ("Error writing the assignments to variables " - "that would drive an execution of the code " - "along the path: %s") % e - raise GameTimeError(err_msg) + errMsg = ("Error writing the assignments to variables " + "that would drive an execution of the code " + "along the path: %s") % e + raise GameTimeError(errMsg) else: - with assignment_file_handler: - assignment_file_handler.write(self.get_assignments()) + with assignmentFileHandler: + assignmentFileHandler.write(self.getAssignments()) @staticmethod - def read_assignments_from_file(location: str) -> Dict[str, str]: + def readAssignmentsFromFile(location): """ Reads, from a file, the assignments to variables that would drive an execution of the code under analysis along a path. - Parameters: - location: str : - Location of the file - Returns: - Dict[str, str]: - Dictionary of assignments to variables that would - drive an execution of the code under analysis along a path. + Arguments: + location: + Location of the file. + Returns: + Dictionary of assignments to variables that would + drive an execution of the code under analysis along a path. """ try: - assignment_file_handler = open(location, "r") + assignmentFileHandler = open(location, "r") except EnvironmentError as e: - err_msg = ("Error reading the assignments to variables " - "that would drive an execution of the code " - "along a path: %s") % e - raise GameTimeError(err_msg) + errMsg = ("Error reading the assignments to variables " + "that would drive an execution of the code " + "along a path: %s") % e + raise GameTimeError(errMsg) else: assignments = {} - with assignment_file_handler: - assignment_file_lines = assignment_file_handler.readlines() - for line in assignment_file_lines: + with assignmentFileHandler: + assignmentFileLines = assignmentFileHandler.readlines() + for line in assignmentFileLines: (variable, assignment) = \ - line.strip().replace(";", "").split(" = ") + line.strip().replace(";", "").split(" = ") assignments[variable] = assignment return assignments - def set_predicted_value(self, value: float): - """ - Sets the predicted value (runtime, energy consumption, etc.) + def setPredictedValue(self, value): + """Sets the predicted value (runtime, energy consumption, etc.) of this path. - Parameters: - value: float : - Value to set as the predicted value of this path - + Arguments: + value: + Value to set as the predicted value of this path. """ - self.predicted_value = value + self.predictedValue = value - def get_predicted_value(self) -> str: + def getPredictedValue(self): """ Returns: - str - String representation of the predicted value - (runtime, energy consumption, etc.) of this path. - + String representation of the predicted value + (runtime, energy consumption, etc.) of this path. """ - return "%g" % self.predicted_value + return "%g" % self.predictedValue - def write_predicted_value_to_file(self, location: str) -> None: - """ - Writes the predicted value of this path to a file. - - Parameters: - location: str : - Location of the file + def writePredictedValueToFile(self, location): + """Writes the predicted value of this path to a file. + + Arguments: + location: + Location of the file. """ try: - predicted_value_file_handler = open(location, "w") + predictedValueFileHandler = open(location, "w") except EnvironmentError as e: - err_msg = ("Error writing the predicted value of the path " - "to the file located at %s: %s" % (location, e)) - raise GameTimeError(err_msg) + errMsg = ("Error writing the predicted value of the path " + "to the file located at %s: %s" % (location, e)) + raise GameTimeError(errMsg) else: - with predicted_value_file_handler: - predicted_value_file_handler.write(self.get_predicted_value()) + with predictedValueFileHandler: + predictedValueFileHandler.write(self.getPredictedValue()) @staticmethod - def read_predicted_value_from_file(location: str) -> float: - """ - Reads the predicted value of a path from a file. + def readPredictedValueFromFile(location): + """Reads the predicted value of a path from a file. - Parameters: - location: str : - Location of the file + Arguments: + location: + Location of the file. Returns: - float: - Predicted value of a path, represented as a number - (either an integer or a floating-point number). - + Predicted value of a path, represented as a number + (either an integer or a floating-point number). """ try: - predicted_value_file_handler = open(location, "r") + predictedValueFileHandler = open(location, "r") except EnvironmentError as e: - err_msg = ("Error reading the predicted value of a path from " - "the file located at %s: %s" % (location, e)) - raise GameTimeError(err_msg) + errMsg = ("Error reading the predicted value of a path from " + "the file located at %s: %s" % (location, e)) + raise GameTimeError(errMsg) else: - with predicted_value_file_handler: - line = predicted_value_file_handler.readline().strip() + with predictedValueFileHandler: + line = predictedValueFileHandler.readline().strip() try: result = int(line) except ValueError: try: result = float(line) except ValueError: - err_msg = ("The following line, in the file located " - "at %s, does not represent a valid " - "predicted value of a path: %s" % - (location, line)) - raise GameTimeError(err_msg) + errMsg = ("The following line, in the file located " + "at %s, does not represent a valid " + "predicted value of a path: %s" % + (location, line)) + raise GameTimeError(errMsg) return result - def set_measured_value(self, value: float) -> None: - """ - Sets the measured value (runtime, energy consumption, etc.) + def setMeasuredValue(self, value): + """Sets the measured value (runtime, energy consumption, etc.) of this path. - Parameters: - value: float : - Value to set as the measured value of this path - + Arguments: + value: + Value to set as the measured value of this path. """ - self.measured_value = value + self.measuredValue = value - def get_measured_value(self) -> str: + def getMeasuredValue(self): """ Returns: - str: - String representation of the measured value - (runtime, energy consumption, etc.) of this path. - + String representation of the measured value + (runtime, energy consumption, etc.) of this path. """ - return "%g" % self.measured_value + return "%g" % self.measuredValue - def write_measured_value_to_file(self, location: str) -> None: - """ - Writes the measured value of this path to a file. - - Parameters: - location: str : - Location of the file + def writeMeasuredValueToFile(self, location): + """Writes the measured value of this path to a file. + Arguments: + location: + Location of the file. """ try: - measured_value_file_handler = open(location, "w") + measuredValueFileHandler = open(location, "w") except EnvironmentError as e: - err_msg = ("Error writing the measured value of the path " - "to the file located at %s: %s" % (location, e)) - raise GameTimeError(err_msg) + errMsg = ("Error writing the measured value of the path " + "to the file located at %s: %s" % (location, e)) + raise GameTimeError(errMsg) else: - with measured_value_file_handler: - measured_value_file_handler.write(self.get_measured_value()) + with measuredValueFileHandler: + measuredValueFileHandler.write(self.getMeasuredValue()) @staticmethod - def read_measured_value_from_file(location: str) -> float: - """ - Reads the measured value of a path from a file. + def readMeasuredValueFromFile(location): + """Reads the measured value of a path from a file. - Parameters: - location: str : - Location of the file + Arguments: + location: + Location of the file. Returns: - float: - Measured value of a path, represented as a number - (either an integer or a floating-point number). - + Measured value of a path, represented as a number + (either an integer or a floating-point number). """ try: - measured_value_file_handler = open(location, "r") + measuredValueFileHandler = open(location, "r") except EnvironmentError as e: - err_msg = ("Error reading the measured value of a path from " - "the file located at %s: %s" % (location, e)) - raise GameTimeError(err_msg) + errMsg = ("Error reading the measured value of a path from " + "the file located at %s: %s" % (location, e)) + raise GameTimeError(errMsg) else: - with measured_value_file_handler: - line = measured_value_file_handler.readline().strip() + with measuredValueFileHandler: + line = measuredValueFileHandler.readline().strip() try: result = int(line) except ValueError: try: result = float(line) except ValueError: - err_msg = ("The following line, in the file located " - "at %s, does not represent a valid " - "measured value of a path: %s" % - (location, line)) - raise GameTimeError(err_msg) + errMsg = ("The following line, in the file located " + "at %s, does not represent a valid " + "measured value of a path: %s" % + (location, line)) + raise GameTimeError(errMsg) return result def __str__(self): result = [] result.append("*** Node IDs ***") - result.append("%s\n" % self.get_nodes()) + result.append("%s\n" % self.getNodes()) result.append("*** Line numbers ***") - result.append("%s\n" % self.get_line_numbers()) + result.append("%s\n" % self.getLineNumbers()) result.append("*** Conditions ***") - result.append("%s\n" % self.get_conditions()) + result.append("%s\n" % self.getConditions()) result.append("*** Condition truths ***") - result.append(self.get_condition_truths()) + result.append(self.getConditionTruths()) result.append("*** Array accesses ***") - result.append(self.get_array_accesses()) + result.append(self.getArrayAccesses()) result.append("*** Aggregate access index expressions ***") - result.append(self.get_agg_index_exprs()) + result.append(self.getAggIndexExprs()) + result.append("*** SMT query ***") + result.append("%s" % self.smtQuery) result.append("*** Assignments ***") - result.append(self.get_assignments()) - result.append("*** Predicted value: %s ***" % self.get_predicted_value()) - result.append("*** Measured value: %s ***" % self.get_measured_value()) + result.append(self.getAssignments()) + result.append("*** Predicted value: %s ***" % self.getPredictedValue()) + result.append("*** Measured value: %s ***" % self.getMeasuredValue()) result.append("") return "\n".join(result) diff --git a/src/path_generator.py b/src/pathGenerator.py similarity index 51% rename from src/path_generator.py rename to src/pathGenerator.py index 61084c5d..d7febb04 100644 --- a/src/path_generator.py +++ b/src/pathGenerator.py @@ -13,13 +13,13 @@ import time -import nx_helper -import pulp_helper +import nxHelper +import pulpHelper from defaults import logger -from gametime_error import GameTimeError -from nx_helper import Dag -from path import Path +from gametimeError import GameTimeError +from nxHelper import Dag +from smt.query import Satisfiability class PathType(object): @@ -41,78 +41,68 @@ class PathType(object): ALL_INCREASING = 4 @staticmethod - def get_description(path_type): + def getDescription(pathType): """ - - Parameters: - path_type : - One of the predefined path types - - Returns: One-word description of the path type provided. - """ - return ("worst" if path_type is PathType.WORST_CASE else - "best" if path_type is PathType.BEST_CASE else - "random" if path_type is PathType.RANDOM else - "all-dec" if path_type is PathType.ALL_DECREASING else - "all-inc" if path_type is PathType.ALL_INCREASING else "") + return ("worst" if pathType is PathType.WORST_CASE else + "best" if pathType is PathType.BEST_CASE else + "random" if pathType is PathType.RANDOM else + "all-dec" if pathType is PathType.ALL_DECREASING else + "all-inc" if pathType is PathType.ALL_INCREASING else "") class PathGenerator(object): - """ - Exposes static methods to generate objects of the ``Path`` class that + """Exposes static methods to generate objects of the ``Path`` class that represent different types of feasible paths in the code being analyzed. - + This class is closely related to the ``Analyzer`` class: except for the private helper methods, all of the static methods can also be accessed as instance methods of an ``Analyzer`` object. - + These methods are maintained in this class to keep the codebase cleaner and more modular. Instances of this class should not need to be made. - """ @staticmethod - def generate_paths(analyzer, num_paths=10, path_type=PathType.WORST_CASE, - interval=None, use_ob_extraction=False): - """ - Generates a list of feasible paths of the code being analyzed, + def generatePaths(analyzer, numPaths=5, pathType=PathType.WORST_CASE, + interval=None, useObExtraction=False): + """Generates a list of feasible paths of the code being analyzed, each represented by an object of the ``Path`` class. - - The type of the generated paths is determined by the path_type + + The type of the generated paths is determined by the pathType argument, which is a class variable of the ``PathType`` class. By default, this argument is ``PathType.WORST_CASE``. For a description of the types, refer to the documentation of the ``PathType`` class. - - The ``num_paths`` argument is an upper bound on how many paths should + + The ``numPaths`` argument is an upper bound on how many paths should be generated, which is 5 by default. This argument is ignored if this method is used to generate all of the feasible paths of the code being analyzed. - + The ``interval`` argument is an ``Interval`` object that represents the interval of values that the generated paths can have. If no ``Interval`` object is provided, the interval of values is considered to be all real numbers. - + This method is idempotent: a second call to this method will produce the same list of ``Path`` objects as the first call, assuming that nothing has changed between the two calls. - + Precondition: The basis ``Path`` objects of the input ``Analyzer`` object have values associated with them. Refer to either the method ``loadBasisValuesFromFile`` or the method ``loadBasisValues`` in the ``Analyzer`` class. - Parameters: + Arguments: analyzer: ``Analyzer`` object that maintains information about the code being analyzed. - num_paths: + numPaths: Upper bound on the number of paths to generate. - path_type: + pathType: Type of paths to generate, represented by a class variable of the ``PathType`` class. The different types of paths are described in the documentation of the ``PathType`` class. @@ -121,79 +111,76 @@ def generate_paths(analyzer, num_paths=10, path_type=PathType.WORST_CASE, values that the generated paths can have. If no ``Interval`` object is provided, the interval of values is considered to be all real numbers. - use_ob_extraction: + useObExtraction: Boolean value specifiying whether to use overcomplete basis extraction algorithm Returns: - List[Path] - List of feasible paths of the code being analyzed, each - represented by an object of the ``Path`` class. - + List of feasible paths of the code being analyzed, each + represented by an object of the ``Path`` class. """ paths = None - if path_type == PathType.WORST_CASE: + if pathType == PathType.WORST_CASE: logger.info("Generating %d worst-case feasible paths..." % - num_paths) + numPaths) paths = \ - PathGenerator._generate_paths(analyzer, num_paths, - pulp_helper.Extremum.LONGEST, - interval, use_ob_extraction) - elif path_type == PathType.BEST_CASE: + PathGenerator._generatePaths(analyzer, numPaths, + pulpHelper.Extremum.LONGEST, + interval, useObExtraction) + elif pathType == PathType.BEST_CASE: logger.info("Generating %d best-case feasible paths..." % - num_paths) + numPaths) paths = \ - PathGenerator._generate_paths(analyzer, num_paths, - pulp_helper.Extremum.SHORTEST, - interval, use_ob_extraction) + PathGenerator._generatePaths(analyzer, numPaths, + pulpHelper.Extremum.SHORTEST, + interval, useObExtraction) if paths is not None: logger.info("%d of %d paths have been generated." % - (len(paths), num_paths)) + (len(paths), numPaths)) return paths - if path_type == PathType.ALL_DECREASING: + if pathType == PathType.ALL_DECREASING: logger.info("Generating all feasible paths in decreasing order " "of value...") paths = \ - PathGenerator._generate_paths(analyzer, analyzer.dag.num_paths, - pulp_helper.Extremum.LONGEST, - interval, use_ob_extraction) - elif path_type == PathType.ALL_INCREASING: + PathGenerator._generatePaths(analyzer, analyzer.dag.numPaths, + pulpHelper.Extremum.LONGEST, + interval, useObExtraction) + elif pathType == PathType.ALL_INCREASING: logger.info("Generating all feasible paths in increasing order " "of value...") paths = \ - PathGenerator._generate_paths(analyzer, analyzer.dag.num_paths, - pulp_helper.Extremum.SHORTEST, - interval, use_ob_extraction) + PathGenerator._generatePaths(analyzer, analyzer.dag.numPaths, + pulpHelper.Extremum.SHORTEST, + interval, useObExtraction) if paths is not None: logger.info("%d feasible paths have been generated." % len(paths)) return paths - if path_type == PathType.RANDOM: + if pathType == PathType.RANDOM: logger.info("Generating random feasible paths...") paths = \ - PathGenerator._generate_paths(analyzer, num_paths, None, interval) + PathGenerator._generatePaths(analyzer, numPaths, None, interval) logger.info("%d of %d paths have been generated." % - (len(paths), num_paths)) + (len(paths), numPaths)) return paths else: - raise GameTimeError("Unrecognized path type: %d" % path_type) + raise GameTimeError("Unrecognized path type: %d" % pathType) @staticmethod - def _generate_paths(analyzer, num_paths, - extremum=pulp_helper.Extremum.LONGEST, - interval=None, use_ob_extraction=False): - """ - Helper static method for the ``generatePaths`` static method. + def _generatePaths(analyzer, numPaths, + extremum=pulpHelper.Extremum.LONGEST, + interval=None, useObExtraction=False): + """Helper static method for the ``generatePaths`` static method. Generates a list of feasible paths of the code being analyzed, each represented by an object of the ``Path`` class. - Parameters: + Arguments: analyzer: ``Analyzer`` object that maintains information about the code being analyzed. - num_paths: + numPaths: Upper bound on the number of paths to generate. extremum: Type of paths to calculate (longest or shortest), @@ -204,62 +191,60 @@ def _generate_paths(analyzer, num_paths, values that the generated paths can have. If no ``Interval`` object is provided, the interval of values is considered to be all real numbers. - use_ob_extraction: + useObExtraction: Boolean value specifiying whether to use overcomplete basis extraction algorithm Returns: - List[Path] - List of feasible paths of the code being analyzed, - each represented by an object of the ``Path`` class. - + List of feasible paths of the code being analyzed, + each represented by an object of the ``Path`` class. """ - if nx_helper.has_cycles(analyzer.dag): - logger.log("Loops in the code have been detected.") - logger.log("No feasible paths have been generated.") + if nxHelper.hasCycles(analyzer.dag): + logger.warn("Loops in the code have been detected.") + logger.warn("No feasible paths have been generated.") return [] logger.info("") - start_time = time.perf_counter() + startTime = time.clock() - if use_ob_extraction: - before_time = time.perf_counter() + if useObExtraction: + beforeTime = time.clock() logger.info("Using the new algorithm to extract the longest path") logger.info("Finding Least Compatible Delta") - mu_max = pulp_helper.find_least_compatible_mu_max( - analyzer, analyzer.basis_paths) + muMax = pulpHelper.findLeastCompatibleMuMax( + analyzer, analyzer.basisPaths) logger.info("Found the least mu_max compatible with measurements: " "%.2f in %.2f seconds" % - (mu_max, time.perf_counter()- before_time)) - analyzer.inferred_mu_max = mu_max - - before_time = time.perf_counter() + (muMax, time.clock() - beforeTime)) + analyzer.inferredMuMax = muMax + + beforeTime = time.clock() logger.info("Calculating error bounds in the estimate") - analyzer.error_scale_factor, path, ilp_problem = \ - pulp_helper.find_worst_expressible_path( - analyzer, analyzer.basis_paths, 0) + analyzer.errorScaleFactor, path, ilpProblem = \ + pulpHelper.findWorstExpressiblePath( + analyzer, analyzer.basisPaths, 0) logger.info( "Total maximal error in estimates is 2 x %.2f x %.2f = %.2f" % - (analyzer.error_scale_factor, mu_max, - 2 * analyzer.error_scale_factor * mu_max)) - logger.info("Calculated in %.2f ms" % (time.perf_counter()- before_time)) + (analyzer.errorScaleFactor, muMax, + 2 * analyzer.errorScaleFactor * muMax)) + logger.info("Calculated in %.2f ms" % (time.clock() - beforeTime)) else: - analyzer.estimate_edge_weights() + analyzer.estimateEdgeWeights() - result_paths = [] - current_path_num, num_paths_unsat, num_candidate_paths = 0, 0, 0 - while (current_path_num < num_paths and - num_candidate_paths < analyzer.dag.num_paths): + resultPaths = [] + currentPathNum, numPathsUnsat, numCandidatePaths = 0, 0, 0 + while (currentPathNum < numPaths and + numCandidatePaths < analyzer.dag.numPaths): logger.info("Currently generating path %d..." % - (current_path_num+1)) + (currentPathNum+1)) logger.info("So far, %d candidate paths were found to be " - "unsatisfiable." % num_paths_unsat) + "unsatisfiable." % numPathsUnsat) - if analyzer.path_dimension == 1: - warn_msg = ("Basis matrix has dimensions 1x1. " + if analyzer.pathDimension == 1: + warnMsg = ("Basis matrix has dimensions 1x1. " "There is only one path through the function " "under analysis.") - logger.warning(warn_msg) + logger.warn(warnMsg) logger.info("Finding a candidate path using an integer " "linear program...") @@ -267,74 +252,77 @@ def _generate_paths(analyzer, num_paths, if extremum is None: source, sink = analyzer.dag.source, analyzer.dag.sink - candidate_path_nodes = nx_helper.get_random_path(analyzer.dag, + candidatePathNodes = nxHelper.getRandomPath(analyzer.dag, source, sink) - candidate_path_edges = Dag.get_edges(candidate_path_nodes) - analyzer.add_path_bundled_constraint(candidate_path_edges) + candidatePathEdges = Dag.getEdges(candidatePathNodes) + analyzer.addPathBundledConstraint(candidatePathEdges) - if use_ob_extraction: - candidate_path_nodes, ilp_problem = \ - pulp_helper.find_longest_path_with_delta( - analyzer, analyzer.basis_paths, mu_max, extremum) + if useObExtraction: + candidatePathNodes, ilpProblem = \ + pulpHelper.findLongestPathWithDelta( + analyzer, analyzer.basisPaths, muMax, extremum) else: - candidate_path_nodes, ilp_problem = \ - pulp_helper.find_extreme_path(analyzer, + candidatePathNodes, ilpProblem = \ + pulpHelper.findExtremePath(analyzer, extremum if extremum is not None - else pulp_helper.Extremum.LONGEST, + else pulpHelper.Extremum.LONGEST, interval) logger.info("") - if ilp_problem.obj_val is None: + if ilpProblem.objVal is None: if (extremum is not None or - num_candidate_paths == analyzer.dag.num_paths): + numCandidatePaths == analyzer.dag.numPaths): logger.info("Unable to find a new candidate path.") break elif extremum is None: - analyzer.add_path_exclusive_constraint(candidate_path_nodes) - analyzer.reset_path_bundled_constraints() - num_candidate_paths = len(analyzer.path_exclusive_constraints) + analyzer.addPathExclusiveConstraint(candidatePathEdges) + analyzer.resetPathBundledConstraints() + numCandidatePaths = len(analyzer.pathExclusiveConstraints) continue logger.info("Candidate path found.") - logger.info("Running the candidate path to check feasibility and measure run time") - candidate_path_edges = Dag.get_edges(candidate_path_nodes) - candidate_path_value = ilp_problem.obj_val - result_path = Path(ilp_problem=ilp_problem, nodes=candidate_path_nodes) - - logger.info("Candidate path is feasible.") - result_path.set_measured_value(0) - result_path.set_predicted_value(candidate_path_value) - result_paths.append(result_path) - logger.info("Path %d generated." % (current_path_num+1)) - analyzer.add_path_exclusive_constraint(candidate_path_edges) - current_path_num += 1 - num_paths_unsat = 0 - - value = analyzer.measure_path(result_path, f'feasible-path{current_path_num}') - - if value < float('inf'): + candidatePathEdges = Dag.getEdges(candidatePathNodes) + candidatePathValue = ilpProblem.objVal + + logger.info("Checking if candidate path is feasible...") + logger.info("") + resultPath = analyzer.checkFeasibility(candidatePathNodes, + ilpProblem) + querySatisfiability = resultPath.smtQuery.satisfiability + if querySatisfiability == Satisfiability.SAT: logger.info("Candidate path is feasible.") - result_path.set_measured_value(value) - result_path.set_predicted_value(candidate_path_value) - result_paths.append(result_path) - logger.info("Path %d generated." % (current_path_num+1)) - analyzer.add_path_exclusive_constraint(candidate_path_edges) - current_path_num += 1 - num_paths_unsat = 0 - else: + resultPath.setPredictedValue(candidatePathValue) + resultPaths.append(resultPath) + logger.info("Path %d generated." % (currentPathNum+1)) + + # Exclude the path generated from future iterations. + analyzer.addPathExclusiveConstraint(candidatePathEdges) + currentPathNum += 1 + numPathsUnsat = 0 + elif querySatisfiability == Satisfiability.UNSAT: logger.info("Candidate path is infeasible.") - analyzer.add_path_exclusive_constraint(candidate_path_edges) + + logger.info("Finding the edges to exclude...") + unsatCore = resultPath.smtQuery.unsatCore + excludeEdges = resultPath.getEdgesForConditions(unsatCore) + logger.info("Edges to be excluded found.") + logger.info("Adding constraint to exclude these edges...") + if len(excludeEdges) > 0: + analyzer.addPathExclusiveConstraint(excludeEdges) + else: + analyzer.addPathExclusiveConstraint(candidatePathEdges) logger.info("Constraint added.") - num_paths_unsat += 1 - num_candidate_paths += 1 + numPathsUnsat += 1 + + numCandidatePaths += 1 if extremum is None: - analyzer.reset_path_bundled_constraints() + analyzer.resetPathBundledConstraints() logger.info("") logger.info("") - analyzer.reset_path_exclusive_constraints() + analyzer.resetPathExclusiveConstraints() logger.info("Time taken to generate paths: %.2f seconds." % - (time.perf_counter() - start_time)) - return result_paths \ No newline at end of file + (time.clock() - startTime)) + return resultPaths diff --git a/src/path_analyzer.py b/src/path_analyzer.py deleted file mode 100644 index d7f16af3..00000000 --- a/src/path_analyzer.py +++ /dev/null @@ -1,88 +0,0 @@ -import os -import re -import file_helper -from nx_helper import Dag -from path import Path -from project_configuration import ProjectConfiguration -from backend.backend import Backend -from smt_solver.extract_labels import find_labels -from smt_solver.smt import run_smt - -class PathAnalyzer(object): - - def __init__(self, preprocessed_path: str, project_config: ProjectConfiguration, dag: Dag, path: Path, path_name: str, repeat: int = 1): - """ - used to run the entire simulation on the given path. - - Parameters: - preprocessed_path : - the path to file being analyzed - project_config : - configuration of gametime - dag : - DAG representation of file being analyzed - path : - Path object corresponding to the path to drive - path_name : - all output files will be in folder with path_name; all generated files will have name path_name + "-gt" - """ - - self.preprocessed_path: str = preprocessed_path - self.project_config: ProjectConfiguration = project_config - self.dag = dag - self.path: Path = path - self.output_folder: str = os.path.join(self.project_config.location_temp_dir, path_name) - self.path_name: str = path_name - self.output_name: str = f'{path_name}-gt' - file_helper.create_dir(self.output_folder) - self.measure_folders: dict[str, str] = {} - bitcode = [] - for node in path.nodes: - bitcode.append(self.dag.get_node_label(self.dag.nodes_indices[node])) - labels_file = find_labels("".join(bitcode), self.output_folder) - - all_labels_file = os.path.join(project_config.location_temp_dir, "labels_0.txt") - with open(all_labels_file, "r") as out_file: - lines = out_file.readlines() - - - all_labels_file = os.path.join(project_config.location_temp_dir, "labels_0.txt") - total_num_labels = 0 - number_line_pattern = re.compile(r'^\s*\d+\s*$') - - with open(all_labels_file, "r") as out_file: - for line_number, line in enumerate(out_file, 1): # Using enumerate to get line number - if number_line_pattern.match(line): # Check if the line matches the pattern - total_num_labels += 1 - else: - raise ValueError(f"Error on line {line_number}: '{line.strip()}' is not a valid line with exactly one number.") - - - self.is_valid = run_smt(self.project_config, labels_file, self.output_folder, total_num_labels) - self.values_filepath = f"{self.output_folder}/klee_input_0_values.txt" - self.repeat = repeat - - def measure_path(self, backend: Backend) -> int: - """ - run the entire simulation on the given path - - Parameters: - backend: Backend : - Backend object used for simulation - - Returns: - the total measurement of path given by backend - """ - if not self.is_valid: - return float('inf') - temp_folder_backend: str = os.path.join(self.output_folder, backend.name) - - if backend.name not in self.measure_folders.keys(): - self.measure_folders[backend.name] = temp_folder_backend - - file_helper.create_dir(temp_folder_backend) - measured_values = [] - for _ in range(self.repeat): - measured_values.append(backend.measure(self.values_filepath, temp_folder_backend)) - return max(measured_values) - diff --git a/src/phoenixHelper.py b/src/phoenixHelper.py new file mode 100644 index 00000000..1414ebfa --- /dev/null +++ b/src/phoenixHelper.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python + +"""Exposes functions to interact with the program analysis +code written in Phoenix. +""" + +"""See the LICENSE file, located in the root directory of +the source distribution and +at http://verifun.eecs.berkeley.edu/gametime/about/LICENSE, +for details on the GameTime license and authors. +""" + + +import os +import subprocess + +from defaults import config +from fileHelper import removeFiles + + +import traceback + +def _generatePhoenixCommand(projectConfig): + """Generates the system call that provides the appropriate arguments + to the Phoenix program analysis code. + + Arguments: + projectConfig: + :class:`~gametime.projectConfiguration.ProjectConfiguration` + object that represents the configuration of a GameTime project. + + Returns: + Appropriate system call as a list that contains the program + to be run and the proper arguments. + """ + locationTempFile = projectConfig.locationTempFile + locationTempFileNoExt, _ = os.path.splitext(locationTempFile) + + command = [] + command.append("cl") + + command.append("-c") + + command.append("-nologo") + command.append("-Ob1") + command.append("-O2") + command.append("-d2plugin:%s" % config.TOOL_PHOENIX) + command.append(locationTempFile) + command.append("-Fo\"%s.obj\"" % locationTempFileNoExt) + command.append("-Fe\"%s.exe\"" % locationTempFileNoExt) + + return command + +def _removeTempPhoenixFiles(projectConfig): + """Removes the temporary files created by the Phoenix analysis code. + + Arguments: + projectConfig: + :class:`~gametime.projectConfiguration.ProjectConfiguration` + object that represents the configuration of a GameTime project. + """ + # By this point, we have files that are named the same as the + # temporary file for GameTime, but that have different extensions. + # Remove these files. + otherTempFiles = r".*-gt\.[^c]+" + removeFiles([otherTempFiles], projectConfig.locationTempDir) + +def _runPhoenix(projectConfig, mode): + """Conducts the sequence of system calls that will run + the Phoenix program analysis code in a specific mode. + + Arguments: + projectConfig: + :class:`~gametime.projectConfiguration.ProjectConfiguration` + object that represents the configuration of a GameTime project. + mode: + Mode that the Phoenix program analysis code should work in. + + Returns: + Zero if the program analysis was successful; a non-zero + value otherwise. + """ + command = _generatePhoenixCommand(projectConfig) + + projectConfig.writeToXmlFile() + procArgs = "\n".join([config.configFile, + projectConfig.locationXmlFile, + mode]) + proc = subprocess.Popen(command, stdin=subprocess.PIPE, shell=True) + proc.communicate(procArgs) + + _removeTempPhoenixFiles(projectConfig) + return proc.returncode + +def createDag(projectConfig): + """Conducts the sequence of system calls that will create the directed + acyclic graph (DAG) that corresponds to the function unit currently + being analyzed. + + Returns: + Zero if the program analysis was successful; a non-zero + value otherwise. + """ + return _runPhoenix(projectConfig, config.TEMP_PHX_CREATE_DAG) + +def findConditions(projectConfig): + """Conducts the sequence of system calls that will find the conditions + along a path in the directed acyclic graph (DAG) that corresponds to + the function unit currently being analyzed. + + Returns: + Zero if the program analysis was successful; a non-zero + value otherwise. + """ + return _runPhoenix(projectConfig, config.TEMP_PHX_FIND_CONDITIONS) diff --git a/src/projectConfiguration.py b/src/projectConfiguration.py new file mode 100644 index 00000000..bb97ea60 --- /dev/null +++ b/src/projectConfiguration.py @@ -0,0 +1,862 @@ +#!/usr/bin/env python + +"""Exposes classes, functions, and modules to maintain information +necessary to configure a GameTime project. +""" + +"""See the LICENSE file, located in the root directory of +the source distribution and +at http://verifun.eecs.berkeley.edu/gametime/about/LICENSE, +for details on the GameTime license and authors. +""" + + +import glob +import os +import re +from xml.dom import minidom + +from defaults import config, logger +from gametimeError import GameTimeError +import pulpHelper + + +class DebugConfiguration(object): + """Stores debugging configuration information, which + determines the debugging information that is shown and + the temporary files that are dumped. + + Attributes: + keepCilTemps: + True if the temporary files that CIL generates during + its analysis are to be kept; False otherwise. + dumpIr: + True if the Phoenix intermediate representation of the function + under analysis is to be dumped to a file; False otherwise. + keepIlpSolverOutput: + True if debugging information and files produced by + the integer linear programming solver are to be kept; + False otherwise. + dumpInstructionTrace: + True if information produced when an IR-level instruction is + traced backward is to be dumped; False otherwise. + dumpPath: + True if information about the path being traced is to be dumped; + False otherwise. + dumpAllPaths: + True if information about all of the paths that have been traced + during analysis are to be dumped to a file; False otherwise. + dumpSmtTrace: + True if information produced during the creation of an SMT query + is to be dumped; False otherwise. + dumpAllQueries: + True if information about all of the SMT queries that have been + made during analysis are to be dumped to a file; False otherwise. + keepParserOutput: + True if the debugging information and temporary files produced by + the parser are to be kept; False otherwise. + keepSimulatorOutput: + True if temporary files produced by a simulator when measuring + the value of a path are to be kept; False otherwise. + """ + + def __init__(self, keepCilTemps=False, dumpIr=False, + keepIlpSolverOutput=False, dumpInstructionTrace=False, + dumpPath=False, dumpAllPaths=False, dumpSmtTrace=False, + dumpAllQueries=False, keepParserOutput=False, + keepSimulatorOutput=False): + #: Keep the temporary files that CIL generates during its analysis. + self.KEEP_CIL_TEMPS = keepCilTemps + + #: Dump the Phoenix intermediate representation of the function + #: under analysis to a file. + self.DUMP_IR = dumpIr + + #: Keep debugging information and files produced by + #: the integer linear programming solver. + self.KEEP_ILP_SOLVER_OUTPUT = keepIlpSolverOutput + + #: Dump information produced when an IR-level instruction + #: is traced backward. + self.DUMP_INSTRUCTION_TRACE = dumpInstructionTrace + + #: Dump information about the path being traced. + self.DUMP_PATH = dumpPath + + #: Dump information about all of the paths that have been traced + #: during analysis to a file. + self.DUMP_ALL_PATHS = dumpAllPaths + + #: Dump information produced when an SMT query is created. + self.DUMP_SMT_TRACE = dumpSmtTrace + + #: Dump information about all of the SMT queries that + #: have been made during analysis to a file. + self.DUMP_ALL_QUERIES = dumpAllQueries + + #: Keep the debugging information and the temporary files + #: produced by the parser. + self.KEEP_PARSER_OUTPUT = keepParserOutput + + #: Keep the temporary files produced by a simulator when + #: measuring the value of a path. + self.KEEP_SIMULATOR_OUTPUT = keepSimulatorOutput + + +class ProjectConfiguration(object): + """Stores information necessary to configure a GameTime project. + + Attributes: + locationFile: + Absolute path of the file to be analyzed. + func: + Name of the function to analyze. + smtSolverName: + Name of the SMT solver used to check the satisfiability of + SMT queries. + startLabel: + Label to start analysis at, if any. + endLabel: + Label to end analysis at, if any. + included: + List of the locations of directories that contain other files + that need to be compiled and linked, but not preprocessed, with + the file that contains the function to be analyzed, + such as header files. + merged: + List of the locations of other files to be merged and preprocessed + with the file that contains the function to be analyzed. + inlined: + List of the names of functions to inline. + unrollLoops: + True if loops present in the function being analyzed are + to be unrolled; False otherwise. + randomizeInitialBasis: + True if the basis that GameTime starts the analysis with + is to be randomized; False otherwise. + maximumErrorScaleFactor: + Maximum error allowed when expressing a path in terms of + basis paths. + determinantThreshold: + Threshold below which the determinant of the basis matrix + is considered "too small". + maxInfeasiblePaths: + Maximum number of infeasible candidate paths that can be + explored before a row of a basis matrix is considered "bad". + modelAsNestedArrays: + True if multi-dimensional arrays should be modeled as + nested arrays, or arrays whose elements can also + be arrays, in an SMT query; False otherwise. + preventBasisRefinement: + True if the refinement of the basis into a 2-barycentric + spanner should be prevented; False otherwise. + ilpSolverName: + Name of the integer linear programming solver used to + solve integer linear programs to generate candidate paths. + debugConfig: + Debugging configuration. + """ + + def __init__(self, locationFile, func, smtSolverName, + startLabel="", endLabel="", included=None, merged=None, + inlined=None, unrollLoops=False, randomizeInitialBasis=False, + maximumErrorScaleFactor = 10, + determinantThreshold=0.001, maxInfeasiblePaths=100, + modelAsNestedArrays=False, preventBasisRefinement=False, + ilpSolverName="", debugConfig=None): + ### FILE INFORMATION ### + # Location of the directory that contains the file to be analyzed. + self.locationOrigDir = "" + + # Location of the file to be analyzed. + self.locationOrigFile = locationFile + + # Location of the file to be analyzed, without the extension. + self.locationOrigNoExtension = "" + + # Name of the file to be analyzed. + self.nameOrigFile = "" + + # Name of the file to be analyzed, without the extension. + self.nameOrigNoExtension = "" + + # Location of the temporary folder that will store the temporary files + # generated by the GameTime toolflow. + self.locationTempDir = "" + + # Pre-constructed location of the temporary file that will be analyzed + # by GameTime. + self.locationTempFile = "" + + # Location of the temporary file that will be analyzed by GameTime, + # without the extension. + self.locationTempNoExtension = "" + + # Name of the temporary file that will be analyzed by GameTime. + self.nameTempFile = "" + + # Name of the temporary file that will be analyzed by GameTime, + # without the extension. + self.nameTempNoExtension = "" + + # Location of the temporary XML file that stores + # the project configuration information. + self.locationXmlFile = "" + + # Name of the temporary XML file that stores + # the project configuration information. + self.nameXmlFile = "" + + # Name of the function to analyze. + self.func = func + + # Label to start analysis at, if any. + self.startLabel = startLabel + + # Label to end analysis at, if any. + self.endLabel = endLabel + + ### PREPROCESSING VARIABLES AND FLAGS ### + # List of the locations of directories that contain other files + # that need to be compiled and linked, but not preprocessed, with + # the file that contains the function to be analyzed, + # such as header files. + self.included = included or [] + + # List of the locations of other files to be merged and preprocessed + # with the file that contains the function to be analyzed. + self.merged = merged or [] + + # List of the names of functions to inline. + self.inlined = inlined or [] + + # Whether to unroll loops present in the function being analyzed. + self.UNROLL_LOOPS = unrollLoops + + ### ANALYSIS VARIABLES AND FLAGS ### + # Whether to randomize the basis that GameTime starts + # the analysis with. + self.RANDOMIZE_INITIAL_BASIS = randomizeInitialBasis + + # Maximum error allowed when expressing a path in terms of + # basis paths. + self.MAXIMUM_ERROR_SCALE_FACTOR = maximumErrorScaleFactor + + # Threshold below which the determinant of the basis matrix + # is considered "too small". + self.DETERMINANT_THRESHOLD = determinantThreshold + + # Maximum number of infeasible candidate paths that can be explored + # before a row of a basis matrix is considered "bad". + self.MAX_INFEASIBLE_PATHS = maxInfeasiblePaths + + # Whether to model multi-dimensional arrays as nested arrays, + # or arrays whose elements can also be arrays, in an SMT query. + self.MODEL_AS_NESTED_ARRAYS = modelAsNestedArrays + + # Whether to prevent the refinement of the basis into + # a 2-barycentric spanner. + self.PREVENT_BASIS_REFINEMENT = preventBasisRefinement + + #TODO: comment here + self.OVER_COMPLETE_BASIS = False + self.OB_EXTRACTION = False + + # PuLP solver object that represents the integer linear + # programming solver used to solve integer linear programs + # to generate candidate paths. + self.ilpSolver = None + + # Solver object that represents the SMT solver used to check + # the satisfiability of SMT queries. + self.smtSolver = None + + # ModelParser object used to parse the models generated by + # the SMT solver in response to satisfiable SMT queries. + self.smtModelParser = None + + ### DEBUGGING ### + # Debugging configuration. + self.debugConfig = debugConfig or DebugConfiguration() + + ### INITIALIZATION ### + # Infer the file path without the file extension. + locationOrigWithExtension = self.locationOrigFile + locationOrigNoExtension, extension = \ + os.path.splitext(locationOrigWithExtension) + + if extension.lower() == ".c": + self.locationOrigNoExtension = locationOrigNoExtension + else: + errMsg = ("Error running the project configuration " + "reader: the name of the file to analyze " + "does not end with a `.c` extension.") + raise GameTimeError(errMsg) + + # Infer the directory that contains the file to analyze. + locationOrigDir = os.path.dirname(locationOrigWithExtension) + self.locationOrigDir = locationOrigDir + + # Infer the name of the file, both with + # and without the extension. + nameOrigFile = os.path.basename(locationOrigWithExtension) + self.nameOrigFile = nameOrigFile + self.nameOrigNoExtension = os.path.splitext(nameOrigFile)[0] + + # Infer the name of the temporary directory where + # GameTime stores its temporary files during its toolflow. + self.locationTempDir = ("%s%s" % + (locationOrigNoExtension, config.TEMP_SUFFIX)) + + # Create the temporary directory, if not already present. + locationTempDir = self.locationTempDir + if not os.path.exists(locationTempDir): + os.mkdir(locationTempDir) + + # Infer the name and location of the temporary file to be analyzed + # by GameTime, both with and without the extension. + nameOrigNoExtension = self.nameOrigNoExtension + nameTempNoExtension = ("%s%s" % + (nameOrigNoExtension, config.TEMP_SUFFIX)) + self.nameTempNoExtension = nameTempNoExtension + nameTempFile = "%s.c" % nameTempNoExtension + self.nameTempFile = nameTempFile + + locationTempFile = \ + os.path.normpath(os.path.join(locationTempDir, nameTempFile)) + self.locationTempFile = locationTempFile + self.locationTempNoExtension = os.path.splitext(locationTempFile)[0] + + # Infer the name and location of the temporary XML file that + # stores the project configuration information. + nameXmlFile = "%s.xml" % config.TEMP_PROJECT_CONFIG + self.nameXmlFile = nameXmlFile + self.locationXmlFile = \ + os.path.normpath(os.path.join(locationTempDir, nameXmlFile)) + + # Initialize the PuLP solver object that interfaces with + # the ILP solver whose name is provided. + self.setIlpSolver(ilpSolverName) + #self.setIlpSolver("cplex") + + # Initialize the Solver and ModelParser objects. + self.setSmtSolverAndModelParser(smtSolverName) + + def setIlpSolver(self, ilpSolverName): + """ + Sets the PuLP solver object associated with this + :class:`~gametime.projectConfiguration.ProjectConfiguration` + object to one that can interface with the integer + linear programming solver whose name is provided. + + Arguments: + ilpSolverName: + Name of an integer linear programming solver. + """ + def _ilpSolverErrMsg(ilpSolverName): + """ + Arguments: + ilpSolverName: + Name of an integer linear programming solver. + + Returns: + Error message that informs the user that the integer + linear programming solver, whose name is provided, cannot + be used for this GameTime project. + """ + if ilpSolverName == "": + return ("The default integer linear programming solver " + "of the PuLP package was not found. " + "This GameTime project cannot use it as its " + "backend integer linear programming solver.") + ilpSolverName = pulpHelper.getProperName(ilpSolverName) + return ("The integer linear programming solver %s " + "was not found. This GameTime project cannot use %s " + "as its backend integer linear programming solver." % + (ilpSolverName, ilpSolverName)) + + ilpSolverName = ilpSolverName.lower() + if not pulpHelper.isIlpSolverName(ilpSolverName): + errMsg = ("Incorrect option specified for the integer " + "linear programming solver: %s") % ilpSolverName + raise GameTimeError(errMsg) + else: + ilpSolver = pulpHelper.getIlpSolver(ilpSolverName, self) + if ilpSolver is not None: + self.ilpSolver = ilpSolver + else: + raise GameTimeError(_ilpSolverErrMsg(ilpSolverName)) + + def setSmtSolverAndModelParser(self, smtSolverName): + """ + Sets the SMT solver and model parser objects associated with this + :class:`~gametime.projectConfiguration.ProjectConfiguration` + object to ones that can interface with the SMT solver + whose name is provided. + + Arguments: + smtSolverName: + Name of an SMT solver. + """ + smtSolverName = smtSolverName.lower() + if smtSolverName.startswith("boolector"): + if config.SOLVER_BOOLECTOR == "": + errMsg = ("The Boolector executable was not found " + "during the configuration of GameTime. " + "This GameTime project cannot use Boolector " + "as its backend SMT solver.") + raise GameTimeError(errMsg) + else: + from smt.solvers.boolectorSolver import BoolectorSolver + from smt.solvers.boolectorSolver import SatSolver + satSolverName = smtSolverName[len("boolector"):] + satSolverName = satSolverName.split("-")[-1] + boolectorSatSolver = \ + SatSolver.getSatSolver(satSolverName) + self.smtSolver = BoolectorSolver(boolectorSatSolver) + + from smt.parsers.boolectorModelParser \ + import BoolectorModelParser + self.smtModelParser = BoolectorModelParser() + elif smtSolverName == "z3": + if config.SOLVER_Z3 == "": + errMsg = ("The Z3 Python frontend was not found " + "during the configuration of GameTime. " + "This GameTime project cannot use Z3 " + "as its backend SMT solver.") + raise GameTimeError(errMsg) + else: + from smt.solvers.z3Solver import Z3Solver + self.smtSolver = Z3Solver() + from smt.parsers.z3ModelParser import Z3ModelParser + self.smtModelParser = Z3ModelParser() + elif smtSolverName == "": + errMsg = "SMT solver not specified." + raise GameTimeError(errMsg) + else: + errMsg = ("Incorrect option specified for " + "the SMT solver: %s") % smtSolverName + raise GameTimeError(errMsg) + + def writeToXmlFile(self, locationXmlFile=None): + """ + Writes the project configuration information to an XML file. + + Arguments: + locationXmlFile: + Location of the XML file. If this is not provided, + the XML file will be located in the temporary + directory where GameTime stores its temporary files. + """ + locationXmlFile = locationXmlFile or self.locationXmlFile + + xmlDoc = minidom.Document() + + # Begin the construction of the XML node tree with the root node. + projectRoot = xmlDoc.createElement("gametime-project") + xmlDoc.appendChild(projectRoot) + + # Create the XML node that stores information about + # the file to be analyzed. + fileNode = xmlDoc.createElement("file") + projectRoot.appendChild(fileNode) + + locationNode = xmlDoc.createElement("location") + locationNode.appendChild(xmlDoc.createTextNode(self.locationOrigFile)) + fileNode.appendChild(locationNode) + + funcNode = xmlDoc.createElement("analysis-function") + funcNode.appendChild(xmlDoc.createTextNode(self.func)) + fileNode.appendChild(funcNode) + + startLabelNode = xmlDoc.createElement("start-label") + startLabelNode.appendChild(xmlDoc.createTextNode(self.startLabel)) + fileNode.appendChild(startLabelNode) + + endLabelNode = xmlDoc.createElement("end-label") + endLabelNode.appendChild(xmlDoc.createTextNode(self.endLabel)) + fileNode.appendChild(endLabelNode) + + # Create the XML node that stores the preprocessing + # variables and flags. + preprocessingNode = xmlDoc.createElement("preprocess") + projectRoot.appendChild(preprocessingNode) + + includeNode = xmlDoc.createElement("include") + includeNode.appendChild(xmlDoc.createTextNode(" ".join(self.included))) + preprocessingNode.appendChild(includeNode) + + mergeNode = xmlDoc.createElement("merge") + mergeNode.appendChild(xmlDoc.createTextNode(" ".join(self.merged))) + preprocessingNode.appendChild(mergeNode) + + inlineNode = xmlDoc.createElement("inline") + inlineNode.appendChild(xmlDoc.createTextNode(" ".join(self.inlined))) + preprocessingNode.appendChild(inlineNode) + + if self.UNROLL_LOOPS: + unrollLoopsNode = xmlDoc.createElement("unroll-loops") + preprocessingNode.appendChild(unrollLoopsNode) + + # Create the XML node that stores the analysis variables and flags. + analysisNode = xmlDoc.createElement("analysis") + projectRoot.appendChild(analysisNode) + + if self.RANDOMIZE_INITIAL_BASIS: + randomizeInitialBasisNode = \ + xmlDoc.createElement("randomize-initial-basis") + analysisNode.appendChild(randomizeInitialBasisNode) + + maximumErrorScaleFactorNode = \ + xmlDoc.createElement("maximum-error-scale-factor") + basisErrorNode = xmlDoc.createTextNode("%g" % + self.MAXIMUM_ERROR_SCALE_FACTOR) + maximumErrorScaleFactorNode.appendChild(basisErrorNode) + + determinantThresholdNode = \ + xmlDoc.createElement("determinant-threshold") + thresholdAmountNode = xmlDoc.createTextNode("%g" % + self.DETERMINANT_THRESHOLD) + determinantThresholdNode.appendChild(thresholdAmountNode) + analysisNode.appendChild(determinantThresholdNode) + + maxInfeasiblePathsNode = xmlDoc.createElement("max-infeasible-paths") + numInfeasiblePathsNode = \ + xmlDoc.createTextNode("%g" % self.MAX_INFEASIBLE_PATHS) + maxInfeasiblePathsNode.appendChild(numInfeasiblePathsNode) + analysisNode.appendChild(maxInfeasiblePathsNode) + + if self.MODEL_AS_NESTED_ARRAYS: + modelAsNestedArraysNode = \ + xmlDoc.createElement("model-as-nested-arrays") + analysisNode.appendChild(modelAsNestedArraysNode) + + if self.PREVENT_BASIS_REFINEMENT: + preventBasisRefinementNode = \ + xmlDoc.createElement("prevent-basis-refinement") + analysisNode.appendChild(preventBasisRefinementNode) + + ilpSolverNode = xmlDoc.createElement("ilp-solver") + ilpSolverName = pulpHelper.getIlpSolverName(self.ilpSolver) + ilpSolverNode.appendChild(xmlDoc.createTextNode(ilpSolverName)) + analysisNode.appendChild(ilpSolverNode) + + smtSolverNode = xmlDoc.createElement("smt-solver") + smtSolverNode.appendChild(xmlDoc.createTextNode(str(self.smtSolver))) + analysisNode.appendChild(smtSolverNode) + + # Create the XML node that stores the debug flags. + debugNode = xmlDoc.createElement("debug") + projectRoot.appendChild(debugNode) + + if self.debugConfig.KEEP_CIL_TEMPS: + keepCilTempsNode = xmlDoc.createElement("keep-cil-temps") + debugNode.appendChild(keepCilTempsNode) + + if self.debugConfig.DUMP_IR: + dumpIrNode = xmlDoc.createElement("dump-ir") + debugNode.appendChild(dumpIrNode) + + if self.debugConfig.KEEP_ILP_SOLVER_OUTPUT: + keepIlpSolverOutputNode = \ + xmlDoc.createElement("keep-ilp-solver-output") + debugNode.appendChild(keepIlpSolverOutputNode) + + if self.debugConfig.DUMP_PATH: + dumpPathNode = xmlDoc.createElement("dump-path") + debugNode.appendChild(dumpPathNode) + + if self.debugConfig.DUMP_ALL_PATHS: + dumpAllPathsNode = xmlDoc.createElement("dump-all-paths") + debugNode.appendChild(dumpAllPathsNode) + + if self.debugConfig.DUMP_INSTRUCTION_TRACE: + dumpInstrTraceNode = xmlDoc.createElement("dump-instruction-trace") + debugNode.appendChild(dumpInstrTraceNode) + + if self.debugConfig.DUMP_SMT_TRACE: + dumpSmtTraceNode = xmlDoc.createElement("dump-smt-trace") + debugNode.appendChild(dumpSmtTraceNode) + + if self.debugConfig.DUMP_ALL_QUERIES: + dumpAllQueriesNode = xmlDoc.createElement("dump-all-queries") + debugNode.appendChild(dumpAllQueriesNode) + + if self.debugConfig.KEEP_PARSER_OUTPUT: + keepParserOutputNode = xmlDoc.createElement("keep-parser-output") + debugNode.appendChild(keepParserOutputNode) + + if self.debugConfig.KEEP_SIMULATOR_OUTPUT: + keepSimulatorOutputNode = \ + xmlDoc.createElement("keep-simulator-output") + debugNode.appendChild(keepSimulatorOutputNode) + + try: + locationHandler = open(locationXmlFile, "w") + except EnvironmentError as e: + errMsg = ("Error creating the project configuration " + "XML file: %s") % e + raise GameTimeError(errMsg) + else: + with locationHandler: + # Create the pretty-printed text version of the XML node tree. + prettyPrinted = xmlDoc.toprettyxml(indent=" ") + locationHandler.write(prettyPrinted) + + +def _getText(node): + """ + Arguments: + node: + Node to extract the text from. + + Returns: + Text from the node provided. + """ + return " ".join(child.data.strip() for child in node.childNodes + if child.nodeType == child.TEXT_NODE) + +def getDirPaths(dirPathsStr, dirLocation=None): + """ + Gets a list of directory paths from the string provided, where + the directory paths are separated by whitespaces or commas. + + Arguments: + dirPathsStr: + String of directory paths. + dirLocation: + Directory to which the directory paths may be relative. + + Returns: + List of directory paths in the string. + """ + dirPaths = re.split(r"[\s,]+", dirPathsStr) + + result = [] + for dirPath in dirPaths: + if dirLocation is not None: + dirPath = os.path.join(dirLocation, dirPath) + result.append(os.path.normpath(dirPath)) + return result + +def getFilePaths(filePathsStr, dirLocation=None): + """ + Gets a list of file paths from the string provided, where the file + paths are separated by whitespaces or commas. The paths can also be + Unix-style globs. + + Arguments: + filePathsStr: + String of file paths. + dirLocation: + Directory to which the file paths may be relative. + + Returns: + List of file paths in the string. + """ + filePaths = re.split(r"[\s,]+", filePathsStr) + + result = [] + for filePath in filePaths: + if dirLocation is not None: + filePath = os.path.join(dirLocation, filePath) + for location in glob.iglob(filePath): + result.append(os.path.normpath(location)) + return result + +def getFuncNames(funcNamesStr): + """ + Gets a list of function names from the string provided, where + the function names are separated by whitespaces or commas. + + Arguments: + funcNamesStr: + String of function names. + + Returns: + List of function names in the string. + """ + return re.split(r"[\s,]+", funcNamesStr) + +def readProjectConfigFile(location): + """ + Reads project configuration information from the XML file provided. + + Arguments: + location: + Location of the XML file that contains project + configuration information. + + Returns: + :class:`~gametime.projectConfiguration.ProjectConfiguration` object + that contains information from the XML file whose location is provided. + """ + logger.info("Reading project configuration in %s..." % location) + + if not os.path.exists(location): + errMsg = "Cannot find project configuration file: %s" % location + raise GameTimeError(errMsg) + + try: + projectConfigDom = minidom.parse(location) + except EnvironmentError as e: + errMsg = "Error reading from project configuration file: %s" % e + raise GameTimeError(errMsg) + + # Check that the root element is properly named. + rootNode = projectConfigDom.documentElement + if rootNode.tagName != 'gametime-project': + raise GameTimeError("The root element in the XML file should be " + "named `gametime-project'.") + + # Check that no child element of the root element has an illegal tag. + rootChildNodes = [node for node in rootNode.childNodes + if node.nodeType == node.ELEMENT_NODE] + for childNode in rootChildNodes: + childNodeTag = childNode.tagName + if childNodeTag not in ["file", "preprocess", "analysis", "debug"]: + raise GameTimeError("Unrecognized tag: %s" % childNodeTag) + + # Find the directory that contains the project configuration XML file. + projectConfigDir = os.path.dirname(os.path.abspath(location)) + + # Initialize the instantiation variables for + # the ProjectConfiguration object. + locationFile, func = "", "" + startLabel, endLabel = "", "" + included, merged, inlined, unrollLoops = [], [], [], False + randomizeInitialBasis = False + maximumErrorScaleFactor = 10 + determinantThreshold, maxInfeasiblePaths = 0.001, 100 + modelAsNestedArrays, preventBasisRefinement = False, False + ilpSolverName, smtSolverName = "", "" + + # Process information about the file to be analyzed. + fileNode = (projectConfigDom.getElementsByTagName("file"))[0] + + for node in fileNode.childNodes: + if node.nodeType == node.ELEMENT_NODE: + nodeText = _getText(node) + nodeTag = node.tagName + + if nodeTag == "location": + locationFile = \ + os.path.normpath(os.path.join(projectConfigDir, nodeText)) + elif nodeTag == "analysis-function": + func = nodeText + elif nodeTag == "start-label": + startLabel = nodeText + elif nodeTag == "end-label": + endLabel = nodeText + else: + raise GameTimeError("Unrecognized tag: %s" % nodeTag) + + # Process the preprocessing variables and flags. + preprocessingNode = \ + (projectConfigDom.getElementsByTagName("preprocess"))[0] + + for node in preprocessingNode.childNodes: + if node.nodeType == node.ELEMENT_NODE: + nodeText = _getText(node) + nodeTag = node.tagName + + if nodeTag == "unroll-loops": + unrollLoops = True + elif nodeTag == "include": + if nodeText != "": + included = getDirPaths(nodeText, projectConfigDir) + elif nodeTag == "merge": + if nodeText != "": + merged = getFilePaths(nodeText, projectConfigDir) + elif nodeTag == "inline": + if nodeText != "": + inlined = getFuncNames(nodeText) + else: + raise GameTimeError("Unrecognized tag: %s" % nodeTag) + + # Process the analysis variables and flags. + analysisNode = (projectConfigDom.getElementsByTagName("analysis"))[0] + + for node in analysisNode.childNodes: + if node.nodeType == node.ELEMENT_NODE: + nodeText = _getText(node) + nodeTag = node.tagName + if nodeTag == "randomize-initial-basis": + randomizeInitialBasis = True + elif nodeTag == "maximum-error-scale-factor": + maximumErrorScaleFactor = float(nodeText) + elif nodeTag == "determinant-threshold": + determinantThreshold = float(nodeText) + elif nodeTag == "max-infeasible-paths": + maxInfeasiblePaths = int(nodeText) + elif nodeTag == "model-as-nested-arrays": + modelAsNestedArrays = True + elif nodeTag == "prevent-basis-refinement": + preventBasisRefinement = True + elif nodeTag == "ilp-solver": + ilpSolverName = nodeText + elif nodeTag == "smt-solver": + smtSolverName = nodeText + else: + raise GameTimeError("Unrecognized tag: %s" % nodeTag) + + # Initialize the instantiation variables for the + # DebugConfiguration object. + keepCilTemps, dumpIr, keepIlpSolverOutput = False, False, False + dumpInstructionTrace, dumpPath, dumpAllPaths = False, False, False + dumpSmtTrace, dumpAllQueries = False, False + keepParserOutput, keepSimulatorOutput = False, False + + # Process the debug flags. + debugNode = (projectConfigDom.getElementsByTagName("debug"))[0] + + for node in debugNode.childNodes: + if node.nodeType == node.ELEMENT_NODE: + nodeText = _getText(node) + nodeTag = node.tagName + + if nodeTag == "keep-cil-temps": + keepCilTemps = True + elif nodeTag == "dump-ir": + dumpIr = True + elif nodeTag == "keep-ilp-solver-output": + keepIlpSolverOutput = True + elif nodeTag == "dump-instruction-trace": + dumpInstructionTrace = True + elif nodeTag == "dump-path": + dumpPath = True + elif nodeTag == "dump-all-paths": + dumpAllPaths = True + elif nodeTag == "dump-smt-trace": + dumpSmtTrace = True + elif nodeTag == "dump-all-queries": + dumpAllQueries = True + elif nodeTag == "keep-parser-output": + keepParserOutput = True + elif nodeTag == "keep-simulator-output": + keepSimulatorOutput = True + else: + raise GameTimeError("Unrecognized tag: %s" % nodeTag) + + # Instantiate a DebugConfiguration object. + debugConfig = DebugConfiguration(keepCilTemps, dumpIr, + keepIlpSolverOutput, dumpInstructionTrace, + dumpPath, dumpAllPaths, dumpSmtTrace, + dumpAllQueries, keepParserOutput, + keepSimulatorOutput) + + # We have obtained all the information we need from the XML + # file provided. Instantiate a ProjectConfiguration object. + projectConfig = ProjectConfiguration(locationFile, func, smtSolverName, + startLabel, endLabel, + included, merged, inlined, unrollLoops, + randomizeInitialBasis, + maximumErrorScaleFactor, + determinantThreshold, + maxInfeasiblePaths, + modelAsNestedArrays, + preventBasisRefinement, + ilpSolverName, debugConfig) + logger.info("Successfully loaded project.") + logger.info("") + return projectConfig diff --git a/src/project_configuration.py b/src/project_configuration.py deleted file mode 100644 index 9eae0efd..00000000 --- a/src/project_configuration.py +++ /dev/null @@ -1,312 +0,0 @@ -#!/usr/bin/env python - -import os - -from gametime_error import GameTimeError -from defaults import config - - -class DebugConfiguration(object): - """ - Stores debugging configuration information, which - determines the debugging information that is shown and - the temporary files that are dumped. - - """ - - def __init__(self, keep_cil_temps=False, dump_ir=False, - keep_ilp_solver_output=False, dump_instruction_trace=False, - dump_path=False, dump_all_paths=False, dump_smt_trace=False, - dump_all_queries=False, keep_parser_output=False, - keep_simulator_output=False): - #: Keep the temporary files that CIL generates during its analysis. - self.KEEP_CIL_TEMPS = keep_cil_temps - - #: Dump the Phoenix intermediate representation of the function - #: under analysis to a file. - self.DUMP_IR = dump_ir - - #: Keep debugging information and files produced by - #: the integer linear programming solver. - self.KEEP_ILP_SOLVER_OUTPUT = keep_ilp_solver_output - - #: Dump information produced when an IR-level instruction - #: is traced backward. - self.DUMP_INSTRUCTION_TRACE = dump_instruction_trace - - #: Dump information about the path being traced. - self.DUMP_PATH = dump_path - - #: Dump information about all_temp_files of the paths that have been traced - #: during analysis to a file. - self.DUMP_ALL_PATHS = dump_all_paths - - #: Dump information produced when an SMT query is created. - self.DUMP_SMT_TRACE = dump_smt_trace - - #: Dump information about all_temp_files of the SMT queries that - #: have been made during analysis to a file. - self.DUMP_ALL_QUERIES = dump_all_queries - - #: Keep the debugging information and the temporary files - #: produced by the parser. - self.KEEP_PARSER_OUTPUT = keep_parser_output - - #: Keep the temporary files produced by a simulator when - #: measuring the value of a path. - self.KEEP_SIMULATOR_OUTPUT = keep_simulator_output - - -class ProjectConfiguration(object): - """ - Stores information necessary to configure a GameTime project. - """ - - def __init__(self, location_file, func, location_additional_files=None, - start_label="", end_label="", included=None, merged=None, - inlined=None, unroll_loops=False, randomize_initial_basis=False, - maximum_error_scale_factor=10, - determinant_threshold=0.001, max_infeasible_paths=100, - model_as_nested_arrays=False, prevent_basis_refinement=False, - ilp_solver_name="", debug_config=None, gametime_flexpret_path="", - gametime_path="", gametime_file_path="", compile_flags=[], backend=""): - ### FILE INFORMATION ### - # Location of the directory that contains the file to be analyzed. - self.location_orig_dir = "" - - # Location of the file to be analyzed. - self.location_orig_file = location_file - - # Location of the additional files to be analyzed. - self.location_additional_files = location_additional_files or [] - - # Location of the file to be analyzed, without the extension. - self.location_orig_no_extension = "" - - # Name of the file to be analyzed. - self.name_orig_file = "" - - # Name of the file to be analyzed, without the extension. - self.name_orig_no_extension = "" - - # Location of the temporary folder that will store the temporary files - # generated by the GameTime toolflow. - self.location_temp_dir = "" - - # Pre-constructed location of the temporary file that will be analyzed - # by GameTime. - self.location_temp_file = "" - - # Location of the temporary file that will be analyzed by GameTime, - # without the extension. - self.location_temp_no_extension = "" - - # Name of the temporary file that will be analyzed by GameTime. - self.name_temp_file = "" - - # Name of the temporary file that will be analyzed by GameTime, - # without the extension. - self.name_temp_no_extension = "" - - # Location of the temporary XML file that stores - # the project configuration information. - self.location_xml_file = "" - - # Name of the temporary XML file that stores - # the project configuration information. - self.name_xml_file = "" - - # Name of the function to analyze. - self.func = func - - # Label to start analysis at, if any. - self.start_label = start_label - - # Label to end analysis at, if any. - self.end_label = end_label - - ### PREPROCESSING VARIABLES AND FLAGS ### - # List of the locations of directories that contain other files - # that need to be compiled and linked, but not preprocessed, with - # the file that contains the function to be analyzed, - # such as header files. - self.included = included or [] - - # List of the locations of other files to be merged and preprocessed - # with the file that contains the function to be analyzed. - self.merged = merged or [] - - # List of the names of functions to inline. - self.inlined = inlined or [] - - # Whether to unroll loops present in the function being analyzed. - self.UNROLL_LOOPS = unroll_loops - - ### ANALYSIS VARIABLES AND FLAGS ### - # Whether to randomize the basis that GameTime starts - # the analysis with. - self.RANDOMIZE_INITIAL_BASIS = randomize_initial_basis - - # Maximum error allowed when expressing a path in terms of - # basis paths. - self.MAXIMUM_ERROR_SCALE_FACTOR = maximum_error_scale_factor - - # Threshold below which the determinant of the basis matrix - # is considered "too small". - self.DETERMINANT_THRESHOLD = determinant_threshold - - # Maximum number of infeasible candidate paths that can be explored - # before a row of a basis matrix is considered "bad". - self.MAX_INFEASIBLE_PATHS = max_infeasible_paths - - # Whether to model multi-dimensional arrays as nested arrays, - # or arrays whose elements can also be arrays, in an SMT query. - self.MODEL_AS_NESTED_ARRAYS = model_as_nested_arrays - - # Whether to prevent the refinement of the basis into - # a 2-barycentric spanner. - self.PREVENT_BASIS_REFINEMENT = prevent_basis_refinement - - # TODO: comment here - self.OVER_COMPLETE_BASIS = False - self.OB_EXTRACTION = False - - # PuLP solver object that represents the integer linear - # programming solver used to solve integer linear programs - # to generate candidate paths. - self.ilp_solver = None - - ### DEBUGGING ### - # Debugging configuration. - self.debug_config = debug_config or DebugConfiguration() - - ### INITIALIZATION ### - # Infer the file path without the file extension. - location_orig_with_extension = self.location_orig_file - location_orig_no_extension, extension = \ - os.path.splitext(location_orig_with_extension) - - if extension.lower() == ".c": - self.location_orig_no_extension = location_orig_no_extension - else: - err_msg = ("Error running the project configuration " - "reader: the name of the file to analyze " - "does not end with a `.c` extension.") - raise GameTimeError(err_msg) - - # Infer the directory that contains the file to analyze. - location_orig_dir = os.path.dirname(location_orig_with_extension) - self.location_orig_dir = location_orig_dir - - # Infer the name of the file, both with - # and without the extension. - name_orig_file = os.path.basename(location_orig_with_extension) - self.name_orig_file = name_orig_file - self.name_orig_no_extension = os.path.splitext(name_orig_file)[0] - - # Infer the name of the temporary directory where - # GameTime stores its temporary files during its toolflow. - self.location_temp_dir = ("%s%s" % - (location_orig_no_extension, config.TEMP_SUFFIX)) - - # Create the temporary directory, if not already present. - location_temp_dir = self.location_temp_dir - if not os.path.exists(location_temp_dir): - os.mkdir(location_temp_dir) - - # Infer the name and location of the temporary file to be analyzed - # by GameTime, both with and without the extension. - name_orig_no_extension = self.name_orig_no_extension - name_temp_no_extension = ("%s%s" % - (name_orig_no_extension, config.TEMP_SUFFIX)) - self.name_temp_no_extension = name_temp_no_extension - name_temp_file = "%s.c" % name_temp_no_extension - self.name_temp_file = name_temp_file - - location_temp_file = \ - os.path.normpath(os.path.join(location_temp_dir, name_temp_file)) - self.location_temp_file = location_temp_file - self.location_temp_no_extension = os.path.splitext(location_temp_file)[0] - - # Infer the name and location of the temporary XML file that - # stores the project configuration information. - name_xml_file = "%s.xml" % config.TEMP_PROJECT_CONFIG - self.name_xml_file = name_xml_file - self.location_xml_file = \ - os.path.normpath(os.path.join(location_temp_dir, name_xml_file)) - - # Initialize the PuLP solver object that interfaces with - # the ILP solver whose name is provided. - self.set_ilp_solver(ilp_solver_name) - # self.setIlpSolver("cplex") - - # Relative path to the FlexPRET repo from the GameTime repo. Needed to run FlexPRET Simulator. - self.gametime_flexpret_path = gametime_flexpret_path - - # Relative path to the GameTime repo from the simulation running folder. Needed to run FlexPRET Simulator. - self.gametime_path = gametime_path - - # Relative path to the GameTime repo from the simulated file. Needed to run FlexPRET Simulator. - self.gametime_file_path = gametime_file_path - - # Additional flags needed when compiling the program - self.compile_flags = compile_flags - - # Backend to execute against - self.backend = backend - - def set_ilp_solver(self, ilp_solver_name): - """ - - Parameters: - ilp_solver_name: str: - ILP solver name to use - - """ - # TODO: Make it real - self.ilp_solver = ilp_solver_name.lower() - - def get_temp_filename_with_extension(self, extension: str, name: str = None) -> str: - """ - Return path of temporary file with name and extension. Extension should - be preceded by a period. For example, calling this function with extension - ".bc" should return something like ".... maingt/main.bc" - - Parameters: - extension: str : - extension of the temporary file name - name: str : - name of the temporary file (defaults to self.nameOrigNoExtension) (Default value = None) - - Returns: - str: - path of the temporary file - - """ - if name is None: - name = self.name_orig_no_extension - filename: str = name + extension - temp_filename: str = os.path.join(self.location_temp_dir, filename) - return temp_filename - - def get_orig_filename_with_extension(self, extension: str, name: str = None) -> str: - """ - Return path of file with name and extension. Extension should - be preceded by a period. For example, calling this function with extension - ".bc" should return something like ".... /main.bc" - - Parameters: - extension: str : - extension of the file - name: str : - name of the file (defaults to self.nameOrigNoExtension) (Default value = None) - Returns: - str: - path of the file in the original directory. - """ - if name is None: - name = self.name_orig_no_extension - filename: str = name + extension - orig_filename: str = os.path.join(self.location_orig_dir, filename) - return orig_filename - diff --git a/src/project_configuration_parser.py b/src/project_configuration_parser.py deleted file mode 100644 index 24b18b3d..00000000 --- a/src/project_configuration_parser.py +++ /dev/null @@ -1,264 +0,0 @@ -#!/usr/bin/env python -import glob -import os -import re -import warnings -from abc import abstractmethod -from typing import Any, List, Type - -from yaml import load - -from defaults import logger -from gametime_error import GameTimeError, GameTimeWarning - -try: - from yaml import CLoader as Loader, CDumper as Dumper -except ImportError: - from yaml import Loader, Dumper - -from project_configuration import ProjectConfiguration, DebugConfiguration - - -class ConfigurationParser(object): - """ """ - - @staticmethod - @abstractmethod - def parse(configuration_file_path: str) -> ProjectConfiguration: - """ - - Parameters: - configuration_file_path: str : - Path to configuration file - - Returns: - ProjectConfiguration: - ProjectConfiguration object corresponding to file. - """ - pass - - -class YAMLConfigurationParser(ConfigurationParser): - """ """ - - @staticmethod - def parse(configuration_file_path: str) -> ProjectConfiguration: - """ - - Parameters: - configuration_file_path: str : - Path to configuration file - - Returns: - ProjectConfiguration: - ProjectConfiguration object corresponding to file. - """ - # Check configuration_file_path exits on the OS - if not os.path.exists(configuration_file_path): - err_msg = "Cannot find project configuration file: %s" % configuration_file_path - raise GameTimeError(err_msg) - - # Read from configuration_file_path into a dict - raw_config: dict[str, Any] = {} - with open(configuration_file_path) as raw_file: - raw_config = load(raw_file, Loader=Loader) - - # Check if yaml file contains gametime-project - if 'gametime-project' not in raw_config.keys(): - err_msg = "Cannot find project configuration in file: %s" % configuration_file_path - raise GameTimeError(err_msg) - - raw_config = raw_config['gametime-project'] - - # Find the directory that contains the project configuration YAML file. - project_config_dir = os.path.dirname(os.path.abspath(configuration_file_path)) - - # Initialize the instantiation variables for - # the ProjectConfiguration object. - location_file, func = "", "" - location_additional_files = [] - start_label, end_label = "", "" - included, merged, inlined, unroll_loops = [], [], [], False - randomize_initial_basis = False - maximum_error_scale_factor = 10 - determinant_threshold, max_infeasible_paths = 0.001, 100 - model_as_nested_arrays, prevent_basis_refinement = False, False - ilp_solver_name = "" - gametime_flexpret_path, gametime_path, gametime_file_path = "","","" - compile_flags = [] - backend = "" - - - # Process information about the file to be analyzed. - file_configs: dict[str, Any] = raw_config.get("file", {}) - for key in file_configs.keys(): - match key: - case "location": - location_file = os.path.normpath(os.path.join(project_config_dir, file_configs[key])) - case "additional-files": - location_additional_files = [os.path.normpath(os.path.join(project_config_dir, additional_file)) for additional_file in file_configs[key]] if file_configs[key] else [] - case "analysis-function": - func = file_configs[key] - case "start-label": - start_label = file_configs[key] - case "end-label": - end_label = file_configs[key] - case _: - warnings.warn("Unrecognized tag : %s" % key, GameTimeWarning) - - # Process the preprocessing variables and flags. - preprocess_configs: dict[str, Any] = raw_config.get("preprocess", {}) - for key in preprocess_configs.keys(): - match key: - case "unroll-loops": - unroll_loops = bool(preprocess_configs[key]) - case "include": - if preprocess_configs[key]: - included = get_dir_paths(preprocess_configs[key], project_config_dir) - case "merge": - if preprocess_configs[key]: - merged = get_file_paths(preprocess_configs[key], project_config_dir) - case "inline": - inlined = preprocess_configs[key] - case "compile_flags": - compile_flags = preprocess_configs[key] - case _: - warnings.warn("Unrecognized tag : %s" % key, GameTimeWarning) - - # Process the analysis variables and flags. - analysis_config: dict[str, Any] = raw_config.get("analysis", {}) - for key in analysis_config.keys(): - match key: - case "randomize-initial-basis": - randomize_initial_basis = bool(analysis_config[key]) - case "maximum-error-scale-factor": - maximum_error_scale_factor = float(analysis_config[key]) - case "determinant-threshold": - determinant_threshold = float(analysis_config[key]) - case "max-infeasible-paths": - max_infeasible_paths = int(analysis_config[key]) - case "model-as-nested-arrays": - model_as_nested_arrays = bool(analysis_config[key]) - case "prevent-basis-refinement": - prevent_basis_refinement = bool(analysis_config[key]) - case "ilp-solver": - ilp_solver_name = analysis_config[key] - case "gametime-flexpret-path": - gametime_flexpret_path = analysis_config[key] - case "gametime-path": - gametime_path = analysis_config[key] - case "gametime-file-path": - gametime_file_path = analysis_config[key] - case "backend": - backend = analysis_config[key] - case _: - warnings.warn("Unrecognized tag : %s" % key, GameTimeWarning) - - # Initialize the instantiation variables for the - # DebugConfiguration object. - keep_cil_temps, dump_ir, keep_ilp_solver_output = False, False, False - dump_instruction_trace, dump_path, dump_all_paths = False, False, False - dump_all_queries = False - keep_parser_output, keep_simulator_output = False, False - - debug_config: dict[str, Any] = raw_config.get("debug", {}) - for key in debug_config.keys(): - value = bool(debug_config[key]) - match key: - case "keep-cil-temps": - keep_cil_temps = value - case "dump-ir": - dump_ir = value - case "keep-ilp-solver-output": - keep_ilp_solver_output = value - case "dump-instruction-trace": - dump_instruction_trace = value - case "dump-path": - dump_path = value - case "dump-all_temp_files-paths": - dump_all_paths = value - case "dump-all_temp_files-queries": - dump_all_queries = value - case "keep-parser-output": - keep_parser_output = value - case "keep-simulator-output": - keep_simulator_output = value - case _: - warnings.warn("Unrecognized tag : %s" % key, GameTimeWarning) - - debug_configuration: DebugConfiguration = DebugConfiguration(keep_cil_temps, dump_ir, - keep_ilp_solver_output, dump_instruction_trace, - dump_path, dump_all_paths, - dump_all_queries, keep_parser_output, - keep_simulator_output) - - project_config: ProjectConfiguration = ProjectConfiguration(location_file, func, location_additional_files, - start_label, end_label, included, - merged, inlined, unroll_loops, - randomize_initial_basis, - maximum_error_scale_factor, - determinant_threshold, max_infeasible_paths, - model_as_nested_arrays, prevent_basis_refinement, - ilp_solver_name, debug_configuration, - gametime_flexpret_path, gametime_path, - gametime_file_path, compile_flags, backend) - logger.info("Successfully loaded project.") - logger.info("") - return project_config - - -def get_dir_paths(dir_paths_str: str, dir_location: str = None) -> List[str]: - """ - Gets a list of directory paths from the string provided, where - the directory paths are separated by whitespaces or commas. - - Parameters: - ir_paths_str: str : - String of directory paths - dir_location: str : - Directory to which the directory paths may be relative (Default value = None) - - Returns: - List[str]: - List of directory paths in the string. - - """ - dir_paths = re.split(r"[\s,]+", dir_paths_str) - - result = [] - for dir_path in dir_paths: - if dir_location is not None: - dir_path = os.path.join(dir_location, dir_path) - result.append(os.path.normpath(dir_path)) - return result - - -def get_file_paths(file_paths_str: str, dir_location: str = None) -> List[str]: - """ - Gets a list of file paths from the string provided, where the file - paths are separated by whitespaces or commas. The paths can also be - Unix-style globs. - - Parameters: - file_paths_str: str : - String of file paths - dir_location: str : - Directory to which the file paths may be relative (Default value = None) - - Returns: - List[str]: - List of file paths in the string. - - """ - file_paths = re.split(r"[\s,]+", file_paths_str) - - result = [] - for file_path in file_paths: - if dir_location is not None: - file_path = os.path.join(dir_location, file_path) - for location in glob.iglob(file_path): - result.append(os.path.normpath(location)) - return result - - -extension_parser_map: dict[str, Type[ConfigurationParser]] = {".yaml": YAMLConfigurationParser} diff --git a/src/pulpHelper.py b/src/pulpHelper.py new file mode 100644 index 00000000..7366aacb --- /dev/null +++ b/src/pulpHelper.py @@ -0,0 +1,982 @@ +#!/usr/bin/env python + +"""Exposes functions to interact with different +linear programming solvers through the PuLP package. +""" + +"""See the LICENSE file, located in the root directory of +the source distribution and +at http://verifun.eecs.berkeley.edu/gametime/about/LICENSE, +for details on the GameTime license and authors. +""" + +import os + +import pulp + +from defaults import logger +from fileHelper import moveFiles +from fileHelper import removeFiles +from interval import Interval + +from nxHelper import Dag + + +class Extremum(object): + """Represents the extremum that needs to be determined.""" + + #: Find the longest path. + LONGEST = 0 + #: Find the shortest path. + SHORTEST = 1 + + +#: Name of the integer linear program constructed. +_LP_NAME = "gt-FindExtremePath" + +#: Dictionary that maps the name of an integer linear programming solver to +#: a list of the PuLP solver classes that can interface with the solver. +_nameIlpSolverMap = { + # Default integer linear programming solver of the PuLP package. + "": ([pulp.LpSolverDefault.__class__] if pulp.LpSolverDefault is not None + else []), + + # CBC mixed integer linear programming solver. + "cbc": [pulp.COIN], + + # Version of CBC provided with the PuLP package. + "cbc-pulp": [pulp.PULP_CBC_CMD], + + # IBM ILOG CPLEX Optimizer. + "cplex": [pulp.CPLEX], + + # GNU Linear Programming Kit (GLPK). + "glpk": [pulp.GLPK, pulp.PYGLPK], + + # Gurobi Optimizer. + "gurobi": [pulp.GUROBI_CMD, pulp.GUROBI], + + # FICO Xpress Optimizer. + "xpress": [pulp.XPRESS], +} + +#: Dictionary that maps the name of an integer linear programming solver, +#: as used by GameTime, to its proper name for display purposes. +_properNameMap = { + "cbc": "CBC", + "cbc-pulp": "CBC (provided with the PuLP package)", + "cplex": "CPLEX", + "glpk": "GLPK", + "gurobi": "Gurobi", + "xpress": "Xpress", +} + +def isIlpSolverName(name): + """ + Arguments: + name: + Possible name of an integer linear programming solver. + + Returns: + `True` if, and only if, the name provided is the name of a supported + integer linear programming solver. + """ + return name in _nameIlpSolverMap + +def getIlpSolverNames(): + """ + Returns: + List of the names of the supported integer linear programming solvers. + """ + return [name for name in _nameIlpSolverMap.keys() if name is not ""] + +def getIlpSolver(ilpSolverName, projectConfig): + """ + Arguments: + ilpSolverName: + Name of the integer linear programming solver. + projectConfig: + :class:`~gametime.projectConfiguration.ProjectConfiguration` + object that represents the configuration of a GameTime project. + + Returns: + PuLP solver object that can interface with the integer + linear programming solver whose name is provided, or `None`, if + no such object can be found. + """ + if not isIlpSolverName(ilpSolverName): + return None + + keepIlpSolverOutput = projectConfig.debugConfig.KEEP_ILP_SOLVER_OUTPUT + for ilpSolverClass in _nameIlpSolverMap[ilpSolverName]: + ilpSolver = ilpSolverClass(keepFiles=keepIlpSolverOutput, + msg=keepIlpSolverOutput) + if ilpSolver.available(): + return ilpSolver + return None + +def getIlpSolverName(ilpSolver): + """ + Arguments: + ilpSolver: + Object of a PuLP solver class. + + Returns: + Name, as used by GameTime, of the integer linear programming + solver that the input PuLP solver object can interface with, + or `None`, if no such name can be found. + """ + ilpSolverClass = ilpSolver.__class__ + for ilpSolverName in _nameIlpSolverMap: + for candidateClass in _nameIlpSolverMap[ilpSolverName]: + if candidateClass == ilpSolverClass: + return ilpSolverName + return None + +def getProperName(ilpSolverName): + """ + Arguments: + ilpSolverName: + Name of an integer linear programming solver, as + used by GameTime. + + Returns: + Proper name of an integer linear programming solver, + for display purposes. + """ + return _properNameMap[ilpSolverName] + +def getIlpSolverProperNames(): + """ + Returns: + List of proper names of the supported integer linear programming + solvers, for display purposes. + """ + return [getProperName(name) for name in getIlpSolverNames()] + + +class IlpProblem(pulp.LpProblem): + """Maintains information about an integer linear programming problem. + It is a subclass of the :class:`~pulp.LpProblem` class of the PuLP + package, and stores additional information relevant to the GameTime + analysis, such as the value of the objective function of the problem. + """ + + def __init__(self, *args, **kwargs): + super(IlpProblem, self).__init__(*args, **kwargs) + + #: Value of the objective function, stored for efficiency purposes. + self.objVal = None + + +def _getEdgeFlowVar(analyzer, edgeFlowVars, edge): + """ + Arguments: + analyzer: + ``Analyzer`` object that maintains information about + the code being analyzed. + edgeFlowVars: + Dictionary that maps a positive integer to a PuLP variable that + represents the flow through an edge. (Each postive integer is + the position of an edge in the list of edges maintained by + the input ``Analyzer`` object.) + edges: + Edge whose corresponding PuLP variable is needed. + + Returns: + PuLP variable that corresponds to the input edge. + """ + return edgeFlowVars[analyzer.dag.edgesIndices[edge]] + +def _getEdgeFlowVars(analyzer, edgeFlowVars, edges): + """ + Arguments: + analyzer: + ``Analyzer`` object that maintains information about + the code being analyzed. + edgeFlowVars: + Dictionary that maps a positive integer to a PuLP variable + that represents the flow through an edge. (Each postive integer + is the position of an edge in the list of edges maintained by + the input analyzer.) + edges: + List of edges whose corresponding PuLP variables are needed. + + Returns: + List of the PuLP variables that correspond to each of + the edges in the input edge list. + """ + return [_getEdgeFlowVar(analyzer, edgeFlowVars, edge) for edge in edges] + +def findLeastCompatibleMuMax(analyzer, paths): + """This function returns the least dealta in the underlying graph, as + specified by 'analyzer', that is feasible with the given set of + measurements as specified by 'paths'. The method does not take into + account which paths are feasible and which not; it considers all the + paths in the graph. + + Arguments: + analyzer: + ``Analyzer`` object that maintains information about + the code being analyzed. + paths: + List of paths used in the measurements. Each path is a list of + edges in the order in which they are visited by the path + + Returns: + A floting point value---the least delta compatible with the + measurements + """ + dag = analyzer.dag + source = dag.source + sink = dag.sink + numEdges = dag.numEdges + edges = dag.edges() + numPaths = len(paths) + + projectConfig = analyzer.projectConfig + + nodesExceptSourceSink = dag.nodesExceptSourceSink + + # Set up the linear programming problem. + logger.info("Number of paths: %d " % numPaths) + logger.info("Setting up the integer linear programming problem...") + problem = IlpProblem(_LP_NAME) + + logger.info("Creating variables") + # Set up the variables that correspond to weights of each edge. + # Each edge is restricted to be a nonnegative real number + edgeWeights = pulp.LpVariable.dicts("we", range(0, numEdges), 0) + # Create the variable that shall correspond to the least delta + delta = pulp.LpVariable("delta", 0) + for path in paths: + pathWeights = \ + _getEdgeFlowVars(analyzer, edgeWeights, dag.getEdges(path.nodes)) + problem += pulp.lpSum(pathWeights) <= delta + path.measuredValue + problem += pulp.lpSum(pathWeights) >= -delta + path.measuredValue + print "LENGTH:", path.measuredValue + + # Optimize for the least delta + problem += delta + logger.info("Finding the minimum value of the objective function...") + + problem.sense = pulp.LpMinimize + problemStatus = problem.solve(solver=projectConfig.ilpSolver) + if problemStatus != pulp.LpStatusOptimal: + logger.info("Maximum value not found.") + return [] + objValMin = pulp.value(delta) + + logger.info("Minimum compatible delta found: %g" % objValMin) + + if projectConfig.debugConfig.KEEP_ILP_SOLVER_OUTPUT: + _moveIlpFiles(os.getcwd(), projectConfig.locationTempDir) + else: + _removeTempIlpFiles() + return objValMin + +#compact +def findLongestPathWithDelta(analyzer, paths, delta, + extremum=Extremum.LONGEST): + """ This functions finds the longest/shortest path compatible with the + measured lengths of paths, as given in 'paths', such the actual + lengths are within 'delta' of the measured lengths + + Arguments: + analyzer: + ``Analyzer`` object that maintains information about + the code being analyzed. + paths: + List of paths used in the measurements. Each path is a list of + edges in the order in which they are visited by the path + delta: + the maximal limit by which the length of a measured path is + allowed to differ from the measured value + extremum: + Specifies whether we are calculating Extremum.LONGEST or + Extremum.SHORTEST + + Returns: + Pair consisting of the resulting path and the ILP problem used to + calculate the path + + """ + # Increase delta by one percent, so that we do end up with an unsatisfiable + # ILP due to floating-point issues + delta *= 1.01 + val, resultPath, problem = generateAndSolveCoreProblem( + analyzer, paths, (lambda path: path.measuredValue + delta), + (lambda path: path.measuredValue - delta), + True, extremum=extremum) + return resultPath, problem + +def makeCompact(dag): + """ Function to create a compact representation of the given graph + Compact means that if in the original graph, there is a simple + path without any branching between two nodes, then in the resulting + graph the entire simple path is replaced by only one edge + + Arguments: + dag: + The graph that get compactified + Returns: + A mapping (vertex, vertex) -> edge_number so that the edge + (vertex, vertex) in the original graph 'dag' gets mapped to + the edge with number 'edge_number'. All edges on a simple path + without branching get mapped to the same 'edge_number' + """ + processed = {} + result = Dag() + source = dag.source + different_edges = [] + edge_map = {} + + def dfs(node, edge_index): + if (node in processed): + return node + processed[node] = node + index = node + neighbors = dag.neighbors(node) + + if len(neighbors) == 0: return + if (len(neighbors) == 1) and (len(dag.predecessors(node)) == 1): + # edge get compactified + edge_map[(node, neighbors[0])] = edge_index + dfs(neighbors[0], edge_index) + return + + for to in dag.neighbors(node): + # start new edge + new_edge = len(different_edges) + different_edges.append(0) + edge_map[(node, to)] = new_edge + dfs(to, new_edge) + return + + dfs(source, source) + return edge_map + + +def generateAndSolveCoreProblem(analyzer, paths, pathFunctionUpper, + pathFunctionLower, weightsPositive, + printProblem=False, extremum=Extremum.LONGEST): + """This function actually constructs the ILP to find the longest path + in the graph specified by 'analyzer' using the set of measured paths given + by 'paths'. + + Arguments + analyzer: + ``Analyzer`` object that maintains information about + the code being analyzed. Among others, contains the underlying + DAG or the collection of infeasible paths. + paths: + List of paths used in the measurements. Each path is a list of + edges in the order in which they are visited by the path + pathFunctionUpper: + Function of type: path -> float that for a given path should + return the upper bound on the length of the given path. The + input 'path' is always from 'paths' + pathFunctionLower: + Function of type: path -> float that for a given path should + return the upper bound on the length of the given path. The + input 'path' is always from 'paths' + weightsPositive: + Boolean value specifying whether the individual edge weight are + required to be at least 0 (if set to True) or can be arbitrary + real value (if set to False) + printProblem: + Boolean value used for debugging. If set to true, the generated + ILP is printed. + extremum: + Specifies whether we are calculating Extremum.LONGEST or + Extremum.SHORTEST + + Returns: + Triple consisting of the length of the longest path found, the actual + path and the ILP problem generated. + """ + dag = analyzer.dag + dag.initializeDictionaries() + source = dag.source + sink = dag.sink + numEdges = dag.numEdges + edges = dag.edges() + numPaths = len(paths) + + # Use the compact representation of the DAG + # compact is now a mapping that for each edge of dag gives an index of an + # edge in the compact graph. + compact = makeCompact(dag) + projectConfig = analyzer.projectConfig + + nodesExceptSourceSink = dag.nodesExceptSourceSink + pathExclusiveConstraints = analyzer.pathExclusiveConstraints + pathBundledConstraints = analyzer.pathBundledConstraints + + # Set up the linear programming problem. + logger.info("Number of paths: %d " % numPaths) + logger.info("Setting up the integer linear programming problem...") + problem = IlpProblem(_LP_NAME) + + + # Take M to be the maximum edge length. Add 1.0 to make sure there are + # no problems due to rounding errors. + M = max([pathFunctionUpper(path) for path in paths] + [0]) + 1.0 + if not weightsPositive: M *= numEdges + + logger.info("Using value %.2f for M --- the maximum edge weight" % M) + logger.info("Creating variables") + + values = set() + for key in compact: + values.add(compact[key]) + new_edges = len(values) + + # Set up the variables that correspond to the flow through each edge. + # Set each of the variables to be an integer binary variable. + edgeFlows = pulp.LpVariable.dicts("EdgeFlow", range(0, new_edges), + 0, 1, pulp.LpBinary) + edgeWeights = pulp.LpVariable.dicts( + "we", range(0, new_edges), 0 if weightsPositive else -M, M) + + # for a given 'path' in the original DAG returns the edgeFlow variables + # corresponding to the edges along the same path in the compact DAG. + def getNewIndices(compact, edgeFlows, path): + edges = [compact[edge] for edge in path] + pathWeights = [edgeFlows[edge] for edge in set(edges)] + return pathWeights + + for path in paths: + pathWeights = \ + getNewIndices(compact, edgeWeights, dag.getEdges(path.nodes)) + problem += pulp.lpSum(pathWeights) <= pathFunctionUpper(path) + problem += pulp.lpSum(pathWeights) >= pathFunctionLower(path) + + + # Add a constraint for the flow from the source. The flow through all of + # the edges out of the source should sum up to exactly 1. + edgeFlowsFromSource = \ + getNewIndices(compact, edgeFlows, dag.out_edges(source)) + problem += pulp.lpSum(edgeFlowsFromSource) == 1, "Flows from source" + + # Add constraints for the rest of the nodes (except sink). The flow + # through all of the edges into a node should equal the flow through + # all of the edges out of the node. Hence, for node n, if e_i and e_j + # enter a node, and e_k and e_l exit a node, the corresponding flow + # equation is e_i + e_j = e_k + e_l. + for node in nodesExceptSourceSink: + if (dag.neighbors(node) == 1) and (dag.predecessors(node) == 1): + continue + edgeFlowsToNode = getNewIndices(compact, edgeFlows, + dag.in_edges(node)) + edgeFlowsFromNode = getNewIndices(compact, edgeFlows, + dag.out_edges(node)) + problem += \ + (pulp.lpSum(edgeFlowsToNode) == pulp.lpSum(edgeFlowsFromNode), + "Flows through %s" % node) + + # Add a constraint for the flow to the sink. The flow through all of + # the edges into the sink should sum up to exactly 1. + edgeFlowsToSink = getNewIndices(compact, edgeFlows, + dag.in_edges(sink)) + problem += pulp.lpSum(edgeFlowsToSink) == 1, "Flows to sink" + + # Add constraints for the exclusive path constraints. To ensure that + # the edges in each constraint are not taken together, the total flow + # through all the edges should add to at least one unit less than + # the number of edges in the constraint. Hence, if a constraint + # contains edges e_a, e_b, e_c, then e_a + e_b + e_c must be less than 3. + # This way, all three of these edges can never be taken together. + for constraintNum, path in enumerate(pathExclusiveConstraints): + edgeFlowsInConstraint = getNewIndices(compact, edgeFlows, path) + problem += (pulp.lpSum(edgeFlowsInConstraint) <= + (len(edgeFlowsInConstraint)-1), + "Path exclusive constraint %d" % (constraintNum+1)) + + + # Each productVars[index] in the longest path should correspond to + # edgeFlows[index] * edgeWeights[index] + productVars = pulp.LpVariable.dicts("pe", range(0, new_edges), -M, M) + for index in range(0, new_edges): + if extremum == Extremum.LONGEST: + problem += productVars[index] <= edgeWeights[index] + problem += productVars[index] <= M * edgeFlows[index] + else: + problem += productVars[index] >= edgeWeights[index] - M * (1.0 - edgeFlows[index]) + problem += productVars[index] >= 0 + + + objective = pulp.lpSum(productVars) + problem += objective + logger.info("Objective function constructed.") + + if extremum == Extremum.LONGEST: + logger.info("Finding the maximum value of the objective function...") + problem.sense = pulp.LpMaximize + else: + logger.info("Finding the minimum value of the objective function...") + problem.sense = pulp.LpMinimize + problemStatus = problem.solve(solver=projectConfig.ilpSolver) + + if (printProblem): logger.info(problem) + + if problemStatus != pulp.LpStatusOptimal: + logger.info("Maximum value not found.") + return -1, [], problem + + objValMax = pulp.value(objective) + problem.objVal = objValMax + logger.info("Maximum value found: %g" % objValMax) + + logger.info("Finding the path that corresponds to the maximum value...") + # Determine the edges along the extreme path using the solution. + maxPath = [edges[edgeNum] for edgeNum in edgeFlows + if edgeFlows[edgeNum].value() > 0.1] + logger.info("Path found.") + + totalLength = sum([productVars[edgeNum].value() for edgeNum in edgeFlows + if edgeFlows[edgeNum].value() == 1]) + logger.info("Total length of the path %.2f" % totalLength) + objValMax = totalLength + + maxPath = [edgeNum for edgeNum in range(0, new_edges) + if edgeFlows[edgeNum].value() > 0.1] + extremePath = [] + #reverse exremePath according to the compact edgeMap + for edge in maxPath: + map_to = [source for source in compact if compact[source] == edge] + extremePath.extend(map_to) + + # Arrange the nodes along the extreme path in order of traversal + # from source to sink. + resultPath = [] + + logger.info("Arranging the nodes along the chosen extreme path " + "in order of traversal...") + # To do so, first construct a dictionary mapping a node along the path + # to the edge from that node. + extremePathDict = {} + for edge in extremePath: + extremePathDict[edge[0]] = edge + # Now, "thread" a path through the dictionary. + currNode = dag.source + resultPath.append(currNode) + while currNode in extremePathDict: + newEdge = extremePathDict[currNode] + currNode = newEdge[1] + resultPath.append(currNode) + logger.info("Nodes along the chosen extreme path arranged.") + + if projectConfig.debugConfig.KEEP_ILP_SOLVER_OUTPUT: + _moveIlpFiles(os.getcwd(), projectConfig.locationTempDir) + else: + _removeTempIlpFiles() + # We're done! + return objValMax, resultPath, problem + + +def findWorstExpressiblePath(analyzer, paths, bound): + """ + Function to find the longest path in the underlying graph of 'analyzer' + assuming the lengths of all measured paths are between -1 and 1. + Arguments + analyzer: + ``Analyzer`` object that maintains information about + the code being analyzed. + paths: + List of paths used in the measurements. Each path is a list of + edges in the order in which they are visited by the path + bound: + ??? + Returns: + Triple consisting of the length of the longsest path, the path itself + and the ILP solved to find the path. + """ + return generateAndSolveCoreProblem( + analyzer, paths, (lambda x: 1), (lambda x: -1), False) + +def findGoodnessOfFit(analyzer, paths, basis): + """ + This function is here only for test purposes. Each path pi in `paths', + can be expressed as a linear combination + pi = a_1 b_1 + ... + a_n b_n + of paths b_i from `basis`. This function returns the least number `c` + such that every path can be expressed as a linear combination of basis + paths b_i such that the sum of absolute value of coefficients is at + most `c`: + |a_1| + |a_2| + ... + |a_n| <= c + Arguments + analyzer: + ``Analyzer`` object that maintains information about + the code being analyzed. + paths: + List of paths that we want to find out how well can be + expressed as a linear combination of paths in `basis` + basis: + List of paths that are used to express `paths` as a linear + combination of + Returns: + The number `c` as described in the paragraph above. + """ + dag = analyzer.dag + source = dag.source + sink = dag.sink + numEdges = dag.numEdges + edges = dag.edges() + numPaths = len(paths) + numBasis = len(basis) + projectConfig = analyzer.projectConfig + + # Set up the linear programming problem. + logger.info("Number of paths: %d " % numPaths) + logger.info("Number of basis paths: %d " % numBasis) + logger.info("Setting up the integer linear programming problem...") + problem = IlpProblem("BLAH") + + logger.info("Creating variables") + indices = [(i, j) for i in range(numPaths) for j in range(numBasis)] + coeffs = pulp.LpVariable.dicts("c", indices, -100, 100) + absValues = pulp.LpVariable.dicts("abs", indices, 0, 100) + bound = pulp.LpVariable("bnd", 0, 10000) + + logger.info("Add absolute values") + for index in indices: + problem += absValues[index] >= coeffs[index] + problem += absValues[index] >= -coeffs[index] + + for i in range(numPaths): + # all coefficients expressing path i + allCoeffExpressing = [absValues[(i, j)] for j in range(numBasis)] + problem += pulp.lpSum(allCoeffExpressing) <= bound + # express path i as a linear combination of basis paths + for edge in edges: + pathsContainingEdge = \ + [j for j in range(numBasis) if (edge in basis[j])] + presentCoeffs = [coeffs[(i, j)] for j in pathsContainingEdge] + present = 1 if (edge in paths[i]) else 0 + problem += pulp.lpSum(presentCoeffs) == present + problem += bound + problem.sense = pulp.LpMinimize + + problemStatus = problem.solve(solver=projectConfig.ilpSolver) + if problemStatus != pulp.LpStatusOptimal: + logger.info("Minimum value not found.") + return [], problem + objValMin = pulp.value(bound) + + logger.info("Minimum value found: %g" % objValMin) + + return objValMin + + +def findMinimalOvercompleteBasis(analyzer, paths, k): + """ + This function is here only for test purposes. The functions finds the + smallest set of 'basis paths' with the following property: Each path pi + in `paths', can be expressed as a linear combination + pi = a_1 b_1 + ... + a_n b_n + of paths b_i from `basis`. This function finds the set of basis paths + such that every path can be expressed as a linear combination of basis + paths b_i such that the sum of absolute value of coefficients is at + most 'k': + |a_1| + |a_2| + ... + |a_n| <= k + Arguments + analyzer: + ``Analyzer`` object that maintains information about + the code being analyzed. + paths: + List of paths that we want to find out how well can be + expressed as a linear combination of paths in `basis` + k: + bound on how well the 'paths' can be expressed as a linear + combination of the calculated basis paths + Returns: + List of paths satisfying the condition stated above + """ + + dag = analyzer.dag + source = dag.soure + sink = dag.sink + numEdges = dag.numEdges + edges = dag.edges() + numPaths = len(paths) + + # Set up the linear programming problem. + logger.info("Number of paths: %d " % numPaths) + logger.info("Setting up the integer linear programming problem...") + problem = IlpProblem(_LP_NAME) + + logger.info("Creating variables") + indices = [(i, j) for i in range(numPaths) for j in range(numPaths)] + coeffs = pulp.LpVariable.dicts("c", indices, -k, k) + absValues = pulp.LpVariable.dicts("abs", indices, 0, k) + usedPaths = pulp.LpVariable.dicts( + "used", range(numPaths), 0, 1, pulp.LpBinary) + + logger.info("Adding usedPaths") + for i in range(numPaths): + for j in range(numPaths): + problem += k * usedPaths[j] >= absValues[(i,j)] + + logger.info("Add absolute values") + for index in indices: + problem += absValues[index] >= coeffs[index] + problem += absValues[index] >= -coeffs[index] + + for i in range(numPaths): + logger.info("Processing path number %d" % i) + # all coefficients expressing path i + allCoeffExpressing = [absValues[(i, j)] for j in range(numPaths)] + problem += pulp.lpSum(allCoeffExpressing) <= k + for edge in edges: + pathsContainingEdge = \ + [j for j in range(numPaths) if (edge in paths[j])] + presentCoeffs = [coeffs[(i, j)] for j in pathsContainingEdge] + present = 1 if (edge in paths[i]) else 0 + problem += pulp.lpSum(presentCoeffs) == present + objective = pulp.lpSum(usedPaths) + problem += objective + problem.sense = pulp.LpMinimize + + problemStatus = problem.solve(solver=projectConfig.ilpSolver) + if problemStatus != pulp.LpStatusOptimal: + logger.info("Minimum value not found.") + return [], problem + objValMin = pulp.value(objective) + + logger.info("Minimum value found: %g" % objValMin) + + solutionPaths = \ + [index for index in range(numPaths) if usedPaths[index].value() == 1] + return solutionPaths + + +def findExtremePath(analyzer, extremum=Extremum.LONGEST, interval=None): + """Determines either the longest or the shortest path through the DAG + with the constraints stored in the ``Analyzer`` object provided. + + Arguments: + analyzer: + ``Analyzer`` object that maintains information about + the code being analyzed. + extremum: + Type of extreme path to calculate. + interval: + ``Interval`` object that represents the interval of values + that the generated paths can have. If no ``Interval`` object + is provided, the interval of values is considered to be + all real numbers. + + Returns: + Tuple whose first element is the longest or the shortest path + through the DAG, as a list of nodes along the path (ordered + by traversal from source to sink), and whose second element is + the integer linear programming problem that was solved to obtain + the path, as an object of the ``IlpProblem`` class. + + If no such path is feasible, given the constraints stored in + the ``Analyzer`` object and the ``Interval`` object provided, + the first element of the tuple is an empty list, and the second + element of the tuple is an ``IlpProblem`` object whose ``objVal`` + instance variable is None. + """ + # Make temporary variables for the frequently accessed + # variables from the ``Analyzer`` object provided. + projectConfig = analyzer.projectConfig + + dag = analyzer.dag + source = dag.source + sink = dag.sink + numEdges = dag.numEdges + + nodesExceptSourceSink = dag.nodesExceptSourceSink + edges = dag.allEdges + edgeWeights = dag.edgeWeights + + pathExclusiveConstraints = analyzer.pathExclusiveConstraints + pathBundledConstraints = analyzer.pathBundledConstraints + + # Set up the linear programming problem. + logger.info("Setting up the integer linear programming problem...") + problem = IlpProblem(_LP_NAME) + + logger.info("Creating the variables and adding the constraints...") + + # Set up the variables that correspond to the flow through each edge. + # Set each of the variables to be an integer binary variable. + edgeFlows = pulp.LpVariable.dicts("EdgeFlow", range(0, numEdges), + 0, 1, pulp.LpBinary) + + # Add a constraint for the flow from the source. The flow through all of + # the edges out of the source should sum up to exactly 1. + edgeFlowsFromSource = _getEdgeFlowVars(analyzer, edgeFlows, + dag.out_edges(source)) + problem += pulp.lpSum(edgeFlowsFromSource) == 1, "Flows from source" + + # Add constraints for the rest of the nodes (except sink). The flow + # through all of the edges into a node should equal the flow through + # all of the edges out of the node. Hence, for node n, if e_i and e_j + # enter a node, and e_k and e_l exit a node, the corresponding flow + # equation is e_i + e_j = e_k + e_l. + for node in nodesExceptSourceSink: + edgeFlowsToNode = _getEdgeFlowVars(analyzer, edgeFlows, + dag.in_edges(node)) + edgeFlowsFromNode = _getEdgeFlowVars(analyzer, edgeFlows, + dag.out_edges(node)) + problem += \ + (pulp.lpSum(edgeFlowsToNode) == pulp.lpSum(edgeFlowsFromNode), + "Flows through %s" % node) + + # Add a constraint for the flow to the sink. The flow through all of + # the edges into the sink should sum up to exactly 1. + edgeFlowsToSink = _getEdgeFlowVars(analyzer, edgeFlows, + dag.in_edges(sink)) + problem += pulp.lpSum(edgeFlowsToSink) == 1, "Flows to sink" + + # Add constraints for the exclusive path constraints. To ensure that + # the edges in each constraint are not taken together, the total flow + # through all the edges should add to at least one unit less than + # the number of edges in the constraint. Hence, if a constraint + # contains edges e_a, e_b, e_c, then e_a + e_b + e_c must be less than 3. + # This way, all three of these edges can never be taken together. + for constraintNum, path in enumerate(pathExclusiveConstraints): + edgeFlowsInConstraint = _getEdgeFlowVars(analyzer, edgeFlows, path) + problem += (pulp.lpSum(edgeFlowsInConstraint) <= (len(path)-1), + "Path exclusive constraint %d" % (constraintNum+1)) + + # Add constraints for the bundled path constraints. If a constraint + # contains edges e_a, e_b, e_c, e_d, and each edge *must* be taken, + # then e_b + e_c + e_d must sum up to e_a, scaled by -3 (or one less + # than the number of edges in the path constraint). Hence, the flow + # constraint is e_b + e_c + e_d = -3 * e_a. By default, we scale + # the first edge in a constraint with this negative value. + for constraintNum, path in enumerate(pathBundledConstraints): + firstEdge = path[0] + firstEdgeFlow = _getEdgeFlowVar(analyzer, edgeFlows, firstEdge) + edgeFlowsForRest = _getEdgeFlowVars(analyzer, edgeFlows, path[1:]) + problem += \ + (pulp.lpSum(edgeFlowsForRest) == (len(path)-1) * firstEdgeFlow, + "Path bundled constraint %d" % (constraintNum+1)) + + # There may be bounds on the values of the paths that are generated + # by this function: we add constraints for these bounds. For this, + # we weight the PuLP variables for the edges using the list of + # edge weights provided, and then impose bounds on the sum. + weightedEdgeFlowVars = [] + for edgeIndex, edgeFlowVar in edgeFlows.items(): + edgeWeight = edgeWeights[edgeIndex] + weightedEdgeFlowVars.append(edgeWeight * edgeFlowVar) + interval = interval or Interval() + if interval.hasFiniteLowerBound(): + problem += \ + (pulp.lpSum(weightedEdgeFlowVars) >= interval.lowerBound) + if interval.hasFiniteUpperBound(): + problem += \ + (pulp.lpSum(weightedEdgeFlowVars) <= interval.upperBound) + + logger.info("Variables created and constraints added.") + + logger.info("Constructing the objective function...") + # Finally, construct and add the objective function. + # We reuse the constraint (possibly) added in the last step of + # the constraint addition phase. + objective = pulp.lpSum(weightedEdgeFlowVars) + problem += objective + logger.info("Objective function constructed.") + + logger.info("Finding the maximum value of the objective function...") + + problem.sense = pulp.LpMaximize + problemStatus = problem.solve(solver=projectConfig.ilpSolver) + if problemStatus != pulp.LpStatusOptimal: + logger.info("Maximum value not found.") + return [], problem + objValMax = pulp.value(objective) + + logger.info("Maximum value found: %g" % objValMax) + + logger.info("Finding the path that corresponds to the maximum value...") + # Determine the edges along the extreme path using the solution. + maxPath = [edges[edgeNum] for edgeNum in edgeFlows + if edgeFlows[edgeNum].value() == 1] + logger.info("Path found.") + + logger.info("Finding the minimum value of the objective function...") + + problem.sense = pulp.LpMinimize + problemStatus = problem.solve(solver=projectConfig.ilpSolver) + if problemStatus != pulp.LpStatusOptimal: + logger.info("Minimum value not found.") + return [], problem + objValMin = pulp.value(objective) + + logger.info("Minimum value found: %g" % objValMin) + + logger.info("Finding the path that corresponds to the minimum value...") + # Determine the edges along the extreme path using the solution. + minPath = [edges[edgeNum] for edgeNum in edgeFlows + if edgeFlows[edgeNum].value() == 1] + logger.info("Path found.") + + # Choose the correct extreme path based on the optimal solutions + # and the type of extreme path required. + absMax, absMin = abs(objValMax), abs(objValMin) + if extremum is Extremum.LONGEST: + extremePath = maxPath if absMax >= absMin else minPath + problem.sense = (pulp.LpMaximize if absMax >= absMin + else pulp.LpMinimize) + problem.objVal = max(absMax, absMin) + elif extremum is Extremum.SHORTEST: + extremePath = minPath if absMax >= absMin else maxPath + problem.sense = (pulp.LpMinimize if absMax >= absMin + else pulp.LpMaximize) + problem.objVal = min(absMax, absMin) + + # Arrange the nodes along the extreme path in order of traversal + # from source to sink. + resultPath = [] + + logger.info("Arranging the nodes along the chosen extreme path " + "in order of traversal...") + # To do so, first construct a dictionary mapping a node along the path + # to the edge from that node. + extremePathDict = {} + for edge in extremePath: + extremePathDict[edge[0]] = edge + # Now, "thread" a path through the dictionary. + currNode = source + resultPath.append(currNode) + while currNode in extremePathDict: + newEdge = extremePathDict[currNode] + currNode = newEdge[1] + resultPath.append(currNode) + logger.info("Nodes along the chosen extreme path arranged.") + + if projectConfig.debugConfig.KEEP_ILP_SOLVER_OUTPUT: + _moveIlpFiles(os.getcwd(), projectConfig.locationTempDir) + else: + _removeTempIlpFiles() + + # We're done! + return resultPath, problem + +def _moveIlpFiles(sourceDir, destDir): + """Moves the files that are generated when an integer linear program + is solved, from the source directory whose location is provided + to the destination directory whose location is provided. + + Arguments: + sourceDir: + Location of the source directory. + destDir: + Location of the destination directory. + """ + moveFiles([_LP_NAME + r"-pulp\.lp", _LP_NAME + r"-pulp\.mps", + _LP_NAME + r"-pulp\.prt", _LP_NAME + r"-pulp\.sol"], + sourceDir, destDir) + +def _removeTempIlpFiles(): + """Removes the temporary files that are generated when an + integer linear program is solved. + """ + removeFiles([r".*\.lp", r".*\.mps", r".*\.prt", r".*\.sol"], os.getcwd()) diff --git a/src/pulp_helper.py b/src/pulp_helper.py deleted file mode 100644 index 8ea8dc23..00000000 --- a/src/pulp_helper.py +++ /dev/null @@ -1,1043 +0,0 @@ -#!/usr/bin/env python - -"""Exposes functions to interact with different -linear programming solvers through the PuLP package. -""" -from __future__ import annotations # remove in 3.11 - -from typing import List, Optional, Dict, Tuple, TYPE_CHECKING - - -if TYPE_CHECKING: - from project_configuration import ProjectConfiguration - from analyzer import Analyzer - -import os - -import pulp - -from defaults import logger -from file_helper import move_files, remove_files -from interval import Interval - -from nx_helper import Dag - -"""See the LICENSE file, located in the root directory of -the source distribution and -at http://verifun.eecs.berkeley.edu/gametime/about/LICENSE, -for details on the GameTime license and authors. -""" - - -class Extremum(object): - """Represents the extremum that needs to be determined.""" - - #: Find the longest path. - LONGEST = 0 - #: Find the shortest path. - SHORTEST = 1 - - -#: Name of the integer linear program constructed. -_LP_NAME = "gt-FindExtremePath" - -#: Dictionary that maps the name of an integer linear programming solver to -#: a list of the PuLP solver classes that can interface with the solver. -_name_ilp_solver_map = { - # Default integer linear programming solver of the PuLP package. - "": ([pulp.LpSolverDefault.__class__] if pulp.LpSolverDefault is not None - else []), - - # CBC mixed integer linear programming solver. - "cbc": [pulp.COIN], - - # Version of CBC provided with the PuLP package. - "cbc-pulp": [pulp.PULP_CBC_CMD], - - # IBM ILOG CPLEX Optimizer. - "cplex": [pulp.CPLEX], - - # GNU Linear Programming Kit (GLPK). - "glpk": [pulp.GLPK, pulp.PYGLPK], - - # Gurobi Optimizer. - "gurobi": [pulp.GUROBI_CMD, pulp.GUROBI], - - # FICO Xpress Optimizer. - "xpress": [pulp.XPRESS], -} - -#: Dictionary that maps the name of an integer linear programming solver, -#: as used by GameTime, to its proper name for display purposes. -_proper_name_map = { - "cbc": "CBC", - "cbc-pulp": "CBC (provided with the PuLP package)", - "cplex": "CPLEX", - "glpk": "GLPK", - "gurobi": "Gurobi", - "xpress": "Xpress", -} - - -def is_ilp_solver_name(name: str)->bool: - """ - Parameters: - name: str : - Possible name of an integer linear programming solver - - Returns: - bool: - `True` if, and only if, the name provided is the name of a supported - integer linear programming solver. - - """ - return name in _name_ilp_solver_map - - -def get_ilp_solver_names() -> List[str]: - """ - Returns: - List[str] - List of the names of the supported integer linear programming solvers. - - """ - return [name for name in _name_ilp_solver_map.keys() if name != ""] - - -def get_ilp_solver(ilp_solver_name: str, project_config: ProjectConfiguration) -> Optional[pulp.LpSolver]: - """ - - Parameters: - ilp_solver_name: str : - Name of the integer linear programming solver - project_config: ProjectConfiguration : - ProjectConfiguration object that represents the configuration of a GameTime project - - Returns: - PuLP solver object that can interface with the integer - linear programming solver whose name is provided, or `None`, if - no such object can be found. - - """ - if not is_ilp_solver_name(ilp_solver_name): - return None - - keep_ilp_solver_output = project_config.debug_config.KEEP_ILP_SOLVER_OUTPUT - for ilp_solver_class in _name_ilp_solver_map[ilp_solver_name]: - ilp_solver = ilp_solver_class(keepFiles=keep_ilp_solver_output, - msg=keep_ilp_solver_output) - if ilp_solver.available(): - return ilp_solver - return None - - -def get_ilp_solver_name(ilp_solver: pulp.LpSolver) -> Optional[str]: - """ - - Parameters: - ilp_solver: pulp.LpSolver : - Object of a PuLP solver class - - Returns: - str, Optional: - Name, as used by GameTime, of the integer linear programming - solver that the input PuLP solver object can interface with, - or `None`, if no such name can be found. - - """ - ilp_solver_class = ilp_solver.__class__ - for ilp_solver_name in _name_ilp_solver_map: - for candidate_class in _name_ilp_solver_map[ilp_solver_name]: - if candidate_class == ilp_solver_class: - return ilp_solver_name - return None - - -def get_proper_name(ilp_solver_name: str) -> str: - """ - - Parameters: - ilp_solver_name: str : - Name of an integer linear programming solver used by GameTime - - Returns: - str: - Proper name of an integer linear programming solver, - for display purposes. - - """ - return _proper_name_map[ilp_solver_name] - - -def get_ilp_solver_proper_names() -> List[str]: - """ - - Returns: - List[str]: - List of proper names of the supported integer linear programming - solvers, for display purposes. - - """ - return [get_proper_name(name) for name in get_ilp_solver_names()] - - -class IlpProblem(pulp.LpProblem): - """ - Maintains information about an integer linear programming problem. - It is a subclass of the `~pulp.LpProblem` class of the PuLP - package, and stores additional information relevant to the GameTime - analysis, such as the value of the objective function of the problem. - """ - - def __init__(self, *args, **kwargs): - super(IlpProblem, self).__init__(*args, **kwargs) - - #: Value of the objective function, stored for efficiency purposes. - self.obj_val = None - - -def _get_edge_flow_var(analyzer: Analyzer, - edge_flow_vars: Dict[int, pulp.LpVariable], - edge: Tuple[str, str]) -> pulp.LpVariable: - """ - Parameters: - analyzer: - ``Analyzer`` object that maintains information about - the code being analyzed. - edge_flow_vars: - Dictionary that maps a positive integer to a PuLP variable that - represents the flow through an edge. (Each postive integer is - the position of an edge in the list of edges maintained by - the input ``Analyzer`` object.) - edge: - Edge whose corresponding PuLP variable is needed. - - - Returns: - PuLP variable that corresponds to the input edge. - - """ - return edge_flow_vars[analyzer.dag.edges_indices[edge]] - - -def _get_edge_flow_vars(analyzer: Analyzer, - edge_flow_vars: Dict[int, pulp.LpVariable], - edges: List[Tuple[str, str]]) -> List[pulp.LpVariable]: - """ - - Parameters: - analyzer: - ``Analyzer`` object that maintains information about - the code being analyzed. - edge_flow_vars: - Dictionary that maps a positive integer to a PuLP variable - that represents the flow through an edge. (Each postive integer - is the position of an edge in the list of edges maintained by - the input analyzer.) - edges: - List of edges whose corresponding PuLP variables are needed. - - - Returns: - List of the PuLP variables that correspond to each of - the edges in the input edge list. - - """ - return [_get_edge_flow_var(analyzer, edge_flow_vars, edge) for edge in edges] - - -def find_least_compatible_mu_max(analyzer: Analyzer, paths): - """ - This function returns the least dealta in the underlying graph, as - specified by 'analyzer', that is feasible with the given set of - measurements as specified by 'paths'. The method does not take into - account which paths are feasible and which not; it considers all_temp_files the - paths in the graph. - - Parameters: - analyzer: - ``Analyzer`` object that maintains information about - the code being analyzed. - paths: - List of paths used in the measurements. Each path is a list of - edges in the order in which they are visited by the path - - Returns: - float: - A floating point value---the least delta compatible with the - measurements - - """ - dag = analyzer.dag - source = dag.source - sink = dag.sink - num_edges = dag.num_edges - edges = dag.edges() - num_paths = len(paths) - - project_config = analyzer.project_config - - nodes_except_source_sink = dag.nodes_except_source_sink - - # Set up the linear programming problem. - logger.info("Number of paths: %d " % num_paths) - logger.info("Setting up the integer linear programming problem...") - problem = IlpProblem(_LP_NAME) - - logger.info("Creating variables") - # Set up the variables that correspond to weights of each edge. - # Each edge is restricted to be a nonnegative real number - edge_weights = pulp.LpVariable.dicts("we", range(0, num_edges), 0) - # Create the variable that shall correspond to the least delta - delta = pulp.LpVariable("delta", 0) - for path in paths: - path_weights = \ - _get_edge_flow_vars(analyzer, edge_weights, dag.get_edges(path.nodes)) - problem += pulp.lpSum(path_weights) <= delta + path.measured_value - problem += pulp.lpSum(path_weights) >= -delta + path.measured_value - print("LENGTH:", path.measured_value) - - # Optimize for the least delta - problem += delta - logger.info("Finding the minimum value of the objective function...") - - problem.sense = pulp.LpMinimize - problem_status = problem.solve(solver=project_config.ilp_solver) - if problem_status != pulp.LpStatusOptimal: - logger.info("Maximum value not found.") - return [] - obj_val_min = pulp.value(delta) - - logger.info("Minimum compatible delta found: %g" % obj_val_min) - - if project_config.debug_config.KEEP_ILP_SOLVER_OUTPUT: - _move_ilp_files(os.getcwd(), project_config.location_temp_dir) - else: - _remove_temp_ilp_files() - return obj_val_min - - -# compact -def find_longest_path_with_delta(analyzer, paths, delta, - extremum=Extremum.LONGEST): - """ - This functions finds the longest/shortest path compatible with the - measured lengths of paths, as given in 'paths', such the actual - lengths are within 'delta' of the measured lengths - - Parameters: - analyzer: - ``Analyzer`` object that maintains information about - the code being analyzed. - paths: - List of paths used in the measurements. Each path is a list of - edges in the order in which they are visited by the path - delta: - the maximal limit by which the length of a measured path is - allowed to differ from the measured value - extremum: - Specifies whether we are calculating Extremum.LONGEST or - Extremum.SHORTEST - - Returns: - Pair consisting of the resulting path and the ILP problem used to - calculate the path - - """ - # Increase delta by one percent, so that we do end up with an unsatisfiable - # ILP due to floating-point issues - delta *= 1.01 - val, result_path, problem = generate_and_solve_core_problem( - analyzer, paths, (lambda path: path.measured_value + delta), - (lambda path: path.measured_value - delta), - True, extremum=extremum) - return result_path, problem - - -def make_compact(dag): - """ - Function to create a compact representation of the given graph - Compact means that if in the original graph, there is a simple - path without any branching between two nodes, then in the resulting - graph the entire simple path is replaced by only one edge - - Parameters: - dag: - The graph that get compactified - - Returns: - A mapping (vertex, vertex) -> edge_number so that the edge - (vertex, vertex) in the original graph 'dag' gets mapped to - the edge with number 'edge_number'. All edges on a simple path - without branching get mapped to the same 'edge_number' - - """ - processed = {} - result = Dag() - source = dag.source - different_edges = [] - edge_map = {} - - def dfs(node, edge_index): - if node in processed: - return node - processed[node] = node - index = node - neighbors = dag.neighbors(node) - - if len(neighbors) == 0: return - if (len(neighbors) == 1) and (len(dag.predecessors(node)) == 1): - # edge get compactified - edge_map[(node, neighbors[0])] = edge_index - dfs(neighbors[0], edge_index) - return - - for to in dag.neighbors(node): - # start new edge - new_edge = len(different_edges) - different_edges.append(0) - edge_map[(node, to)] = new_edge - dfs(to, new_edge) - return - - dfs(source, source) - return edge_map - - -def generate_and_solve_core_problem(analyzer, paths, path_function_upper, - path_function_lower, weights_positive, - print_problem=False, extremum=Extremum.LONGEST): - """ - This function actually constructs the ILP to find the longest path - in the graph specified by 'analyzer' using the set of measured paths given - by 'paths'. - - Parameters: - analyzer: - ``Analyzer`` object that maintains information about - the code being analyzed. Among others, contains the underlying - DAG or the collection of infeasible paths. - paths: - List of paths used in the measurements. Each path is a list of - edges in the order in which they are visited by the path - pathFunctionUpper: - Function of type: path -> float that for a given path should - return the upper bound on the length of the given path. The - input 'path' is always from 'paths' - pathFunctionLower: - Function of type: path -> float that for a given path should - return the upper bound on the length of the given path. The - input 'path' is always from 'paths' - weightsPositive: - Boolean value specifying whether the individual edge weight are - required to be at least 0 (if set to True) or can be arbitrary - real value (if set to False) - printProblem: - Boolean value used for debugging. If set to true, the generated - ILP is printed. - extremum: - Specifies whether we are calculating Extremum.LONGEST or - Extremum.SHORTEST - - Returns: - Triple consisting of the length of the longest path found, the actual - path and the ILP problem generated. - - """ - dag = analyzer.dag - dag.initialize_dictionaries() - source = dag.source - sink = dag.sink - num_edges = dag.num_edges - edges = dag.edges() - num_paths = len(paths) - - # Use the compact representation of the DAG - # compact is now a mapping that for each edge of dag gives an index of an - # edge in the compact graph. - compact = make_compact(dag) - project_config = analyzer.project_config - - nodes_except_source_sink = dag.nodes_except_source_sink - path_exclusive_constraints = analyzer.path_exclusive_constraints - path_bundled_constraints = analyzer.path_bundled_constraints - - # Set up the linear programming problem. - logger.info("Number of paths: %d " % num_paths) - logger.info("Setting up the integer linear programming problem...") - problem = IlpProblem(_LP_NAME) - - # Take M to be the maximum edge length. Add 1.0 to make sure there are - # no problems due to rounding errors. - m = max([path_function_upper(path) for path in paths] + [0]) + 1.0 - if not weights_positive: m *= num_edges - - logger.info("Using value %.2f for M --- the maximum edge weight" % m) - logger.info("Creating variables") - - values = set() - for key in compact: - values.add(compact[key]) - new_edges = len(values) - - # Set up the variables that correspond to the flow through each edge. - # Set each of the variables to be an integer binary variable. - edge_flows = pulp.LpVariable.dicts("EdgeFlow", range(0, new_edges), - 0, 1, pulp.LpBinary) - edge_weights = pulp.LpVariable.dicts( - "we", range(0, new_edges), 0 if weights_positive else -m, m) - - # for a given 'path' in the original DAG returns the edgeFlow variables - # corresponding to the edges along the same path in the compact DAG. - def get_new_indices(compact, edge_flows, path): - edges = [compact[edge] for edge in path] - path_weights = [edge_flows[edge] for edge in set(edges)] - return path_weights - - for path in paths: - path_weights = \ - get_new_indices(compact, edge_weights, dag.get_edges(path.nodes)) - problem += pulp.lpSum(path_weights) <= path_function_upper(path) - problem += pulp.lpSum(path_weights) >= path_function_lower(path) - - # Add a constraint for the flow from the source. The flow through all_temp_files of - # the edges out of the source should sum up to exactly 1. - edge_flows_from_source = \ - get_new_indices(compact, edge_flows, dag.out_edges(source)) - problem += pulp.lpSum(edge_flows_from_source) == 1, "Flows from source" - - # Add constraints for the rest of the nodes (except sink). The flow - # through all_temp_files of the edges into a node should equal the flow through - # all_temp_files of the edges out of the node. Hence, for node n, if e_i and e_j - # enter a node, and e_k and e_l exit a node, the corresponding flow - # equation is e_i + e_j = e_k + e_l. - for node in nodes_except_source_sink: - if (dag.neighbors(node) == 1) and (dag.predecessors(node) == 1): - continue - edge_flows_to_node = get_new_indices(compact, edge_flows, - dag.in_edges(node)) - edge_flows_from_node = get_new_indices(compact, edge_flows, - dag.out_edges(node)) - problem += \ - (pulp.lpSum(edge_flows_to_node) == pulp.lpSum(edge_flows_from_node), - "Flows through %s" % node) - - # Add a constraint for the flow to the sink. The flow through all_temp_files of - # the edges into the sink should sum up to exactly 1. - edge_flows_to_sink = get_new_indices(compact, edge_flows, - dag.in_edges(sink)) - problem += pulp.lpSum(edge_flows_to_sink) == 1, "Flows to sink" - - # Add constraints for the exclusive path constraints. To ensure that - # the edges in each constraint are not taken together, the total flow - # through all_temp_files the edges should add to at least one unit less than - # the number of edges in the constraint. Hence, if a constraint - # contains edges e_a, e_b, e_c, then e_a + e_b + e_c must be less than 3. - # This way, all_temp_files three of these edges can never be taken together. - for constraint_num, path in enumerate(path_exclusive_constraints): - edge_flows_in_constraint = get_new_indices(compact, edge_flows, path) - problem += (pulp.lpSum(edge_flows_in_constraint) <= - (len(edge_flows_in_constraint) - 1), - "Path exclusive constraint %d" % (constraint_num + 1)) - - # Each product_vars[index] in the longest path should correspond to - # edge_flows[index] * edge_weights[index] - product_vars = pulp.LpVariable.dicts("pe", range(0, new_edges), -m, m) - for index in range(0, new_edges): - if extremum == Extremum.LONGEST: - problem += product_vars[index] <= edge_weights[index] - problem += product_vars[index] <= m * edge_flows[index] - else: - problem += product_vars[index] >= edge_weights[index] - m * (1.0 - edge_flows[index]) - problem += product_vars[index] >= 0 - - objective = pulp.lpSum(product_vars) - problem += objective - logger.info("Objective function constructed.") - - if extremum == Extremum.LONGEST: - logger.info("Finding the maximum value of the objective function...") - problem.sense = pulp.LpMaximize - else: - logger.info("Finding the minimum value of the objective function...") - problem.sense = pulp.LpMinimize - problem_status = problem.solve(solver=project_config.ilp_solver) - - if print_problem: logger.info(problem) - - if problem_status != pulp.LpStatusOptimal: - logger.info("Maximum value not found.") - return -1, [], problem - - obj_val_max = pulp.value(objective) - problem.obj_val = obj_val_max - logger.info("Maximum value found: %g" % obj_val_max) - - logger.info("Finding the path that corresponds to the maximum value...") - # Determine the edges along the extreme path using the solution. - max_path = [edges[edge_num] for edge_num in edge_flows - if edge_flows[edge_num].value() > 0.1] - logger.info("Path found.") - - total_length = sum([product_vars[edge_num].value() for edge_num in edge_flows - if edge_flows[edge_num].value() == 1]) - logger.info("Total length of the path %.2f" % total_length) - obj_val_max = total_length - - max_path = [edge_num for edge_num in range(0, new_edges) - if edge_flows[edge_num].value() > 0.1] - extreme_path = [] - # reverse extreme_path according to the compact edgeMap - for edge in max_path: - map_to = [source for source in compact if compact[source] == edge] - extreme_path.extend(map_to) - - # Arrange the nodes along the extreme path in order of traversal - # from source to sink. - result_path = [] - - logger.info("Arranging the nodes along the chosen extreme path " - "in order of traversal...") - # To do so, first construct a dictionary mapping a node along the path - # to the edge from that node. - extreme_path_dict = {} - for edge in extreme_path: - extreme_path_dict[edge[0]] = edge - # Now, "thread" a path through the dictionary. - curr_node = dag.source - result_path.append(curr_node) - while curr_node in extreme_path_dict: - new_edge = extreme_path_dict[curr_node] - curr_node = new_edge[1] - result_path.append(curr_node) - logger.info("Nodes along the chosen extreme path arranged.") - - if project_config.debug_config.KEEP_ILP_SOLVER_OUTPUT: - _move_ilp_files(os.getcwd(), project_config.location_temp_dir) - else: - _remove_temp_ilp_files() - # We're done! - return obj_val_max, result_path, problem - - -def find_worst_expressible_path(analyzer, paths, bound): - """ - Function to find the longest path in the underlying graph of 'analyzer' - assuming the lengths of all_temp_files measured paths are between -1 and 1. - - Parameters: - analyzer: - ``Analyzer`` object that maintains information about - the code being analyzed. - paths: - List of paths used in the measurements. Each path is a list of - edges in the order in which they are visited by the path - bound: - ??? - - Returns: - Triple consisting of the length of the longsest path, the path itself - and the ILP solved to find the path. - - """ - return generate_and_solve_core_problem( - analyzer, paths, (lambda x: 1), (lambda x: -1), False) - - -def find_goodness_of_fit(analyzer, paths, basis): - """ - This function is here only for test purposes. Each path pi in `paths', - can be expressed as a linear combination - pi = a_1 b_1 + ... + a_n b_n - of paths b_i from `basis`. This function returns the least number `c` - such that every path can be expressed as a linear combination of basis - paths b_i such that the sum of absolute value of coefficients is at - most `c`: |a_1| + |a_2| + ... + |a_n| <= c - - Parameters: - analyzer: - ``Analyzer`` object that maintains information about - the code being analyzed. - paths: - List of paths that we want to find out how well can be - expressed as a linear combination of paths in `basis` - basis: - List of paths that are used to express `paths` as a linear - combination of - - Returns: - The number `c` as described in the paragraph above. - - """ - dag = analyzer.dag - source = dag.source - sink = dag.sink - num_edges = dag.num_edges - edges = dag.edges() - num_paths = len(paths) - num_basis = len(basis) - project_config = analyzer.project_config - - # Set up the linear programming problem. - logger.info("Number of paths: %d " % num_paths) - logger.info("Number of basis paths: %d " % num_basis) - logger.info("Setting up the integer linear programming problem...") - problem = IlpProblem("BLAH") - - logger.info("Creating variables") - indices = [(i, j) for i in range(num_paths) for j in range(num_basis)] - coeffs = pulp.LpVariable.dicts("c", indices, -100, 100) - abs_values = pulp.LpVariable.dicts("abs", indices, 0, 100) - bound = pulp.LpVariable("bnd", 0, 10000) - - logger.info("Add absolute values") - for index in indices: - problem += abs_values[index] >= coeffs[index] - problem += abs_values[index] >= -coeffs[index] - - for i in range(num_paths): - # all_temp_files coefficients expressing path i - all_coeff_expressing = [abs_values[(i, j)] for j in range(num_basis)] - problem += pulp.lpSum(all_coeff_expressing) <= bound - # express path i as a linear combination of basis paths - for edge in edges: - paths_containing_edge = \ - [j for j in range(num_basis) if (edge in basis[j])] - present_coeffs = [coeffs[(i, j)] for j in paths_containing_edge] - present = 1 if (edge in paths[i]) else 0 - problem += pulp.lpSum(present_coeffs) == present - problem += bound - problem.sense = pulp.LpMinimize - - problem_status = problem.solve(solver=project_config.ilp_solver) - if problem_status != pulp.LpStatusOptimal: - logger.info("Minimum value not found.") - return [], problem - obj_val_min = pulp.value(bound) - - logger.info("Minimum value found: %g" % obj_val_min) - - return obj_val_min - - -def find_minimal_overcomplete_basis(analyzer: Analyzer, paths, k): - """ - This function is here only for test purposes. The functions finds the - smallest set of 'basis paths' with the following property: Each path pi - in `paths', can be expressed as a linear combination - pi = a_1 b_1 + ... + a_n b_n - of paths b_i from `basis`. This function finds the set of basis paths - such that every path can be expressed as a linear combination of basis - paths b_i such that the sum of absolute value of coefficients is at - most 'k': |a_1| + |a_2| + ... + |a_n| <= k - - Parameters: - analyzer: - ``Analyzer`` object that maintains information about - the code being analyzed. - paths: - List of paths that we want to find out how well can be - expressed as a linear combination of paths in `basis` - k: - bound on how well the 'paths' can be expressed as a linear - combination of the calculated basis paths - - - Returns: - List of paths satisfying the condition stated above - - """ - project_config = analyzer.project_config - dag = analyzer.dag - source = dag.source - sink = dag.sink - num_edges = dag.num_edges - edges = dag.edges() - num_paths = len(paths) - - # Set up the linear programming problem. - logger.info("Number of paths: %d " % num_paths) - logger.info("Setting up the integer linear programming problem...") - problem = IlpProblem(_LP_NAME) - - logger.info("Creating variables") - indices = [(i, j) for i in range(num_paths) for j in range(num_paths)] - coeffs = pulp.LpVariable.dicts("c", indices, -k, k) - abs_values = pulp.LpVariable.dicts("abs", indices, 0, k) - used_paths = pulp.LpVariable.dicts( - "used", range(num_paths), 0, 1, pulp.LpBinary) - - logger.info("Adding used_paths") - for i in range(num_paths): - for j in range(num_paths): - problem += k * used_paths[j] >= abs_values[(i, j)] - - logger.info("Add absolute values") - for index in indices: - problem += abs_values[index] >= coeffs[index] - problem += abs_values[index] >= -coeffs[index] - - for i in range(num_paths): - logger.info("Processing path number %d" % i) - # all_temp_files coefficients expressing path i - all_coeff_expressing = [abs_values[(i, j)] for j in range(num_paths)] - problem += pulp.lpSum(all_coeff_expressing) <= k - for edge in edges: - paths_containing_edge = \ - [j for j in range(num_paths) if (edge in paths[j])] - present_coeffs = [coeffs[(i, j)] for j in paths_containing_edge] - present = 1 if (edge in paths[i]) else 0 - problem += pulp.lpSum(present_coeffs) == present - objective = pulp.lpSum(used_paths) - problem += objective - problem.sense = pulp.LpMinimize - - problem_status = problem.solve(solver=project_config.ilp_solver) - if problem_status != pulp.LpStatusOptimal: - logger.info("Minimum value not found.") - return [], problem - obj_val_min = pulp.value(objective) - - logger.info("Minimum value found: %g" % obj_val_min) - - solution_paths = \ - [index for index in range(num_paths) if used_paths[index].value() == 1] - return solution_paths - - -def find_extreme_path(analyzer, extremum=Extremum.LONGEST, interval=None): - """ - Determines either the longest or the shortest path through the DAG - with the constraints stored in the ``Analyzer`` object provided. - - Parameters: - analyzer: - ``Analyzer`` object that maintains information about - the code being analyzed. - extremum: - Type of extreme path to calculate. - interval: - ``Interval`` object that represents the interval of values - that the generated paths can have. If no ``Interval`` object - is provided, the interval of values is considered to be - all_temp_files real numbers. - - Returns: - Tuple whose first element is the longest or the shortest path - through the DAG, as a list of nodes along the path (ordered - by traversal from source to sink), and whose second element is - the integer linear programming problem that was solved to obtain - the path, as an object of the ``IlpProblem`` class. - - If no such path is feasible, given the constraints stored in - the ``Analyzer`` object and the ``Interval`` object provided, - the first element of the tuple is an empty list, and the second - element of the tuple is an ``IlpProblem`` object whose ``obj_al`` - instance variable is None. - - """ - # Make temporary variables for the frequently accessed - # variables from the ``Analyzer`` object provided. - project_config = analyzer.project_config - - dag = analyzer.dag - source = dag.source - sink = dag.sink - num_edges = dag.num_edges - - nodes_except_source_sink = dag.nodes_except_source_sink - edges = list(dag.all_edges) - edge_weights = dag.edge_weights - - path_exclusive_constraints = analyzer.path_exclusive_constraints - path_bundled_constraints = analyzer.path_bundled_constraints - - # Set up the linear programming problem. - logger.info("Setting up the integer linear programming problem...") - problem = IlpProblem(_LP_NAME) - - logger.info("Creating the variables and adding the constraints...") - - # Set up the variables that correspond to the flow through each edge. - # Set each of the variables to be an integer binary variable. - edge_flows = pulp.LpVariable.dicts("EdgeFlow", range(0, num_edges), - 0, 1, pulp.LpBinary) - - # Add a constraint for the flow from the source. The flow through all_temp_files of - # the edges out of the source should sum up to exactly 1. - edge_flows_from_source = _get_edge_flow_vars(analyzer, edge_flows, - dag.out_edges(source)) - problem += pulp.lpSum(edge_flows_from_source) == 1, "Flows from source" - - # Add constraints for the rest of the nodes (except sink). The flow - # through all_temp_files of the edges into a node should equal the flow through - # all_temp_files of the edges out of the node. Hence, for node n, if e_i and e_j - # enter a node, and e_k and e_l exit a node, the corresponding flow - # equation is e_i + e_j = e_k + e_l. - for node in nodes_except_source_sink: - edge_flows_to_node = _get_edge_flow_vars(analyzer, edge_flows, - dag.in_edges(node)) - edge_flows_from_node = _get_edge_flow_vars(analyzer, edge_flows, - dag.out_edges(node)) - problem += \ - (pulp.lpSum(edge_flows_to_node) == pulp.lpSum(edge_flows_from_node), - "Flows through %s" % node) - - # Add a constraint for the flow to the sink. The flow through all_temp_files of - # the edges into the sink should sum up to exactly 1. - edge_flows_to_sink = _get_edge_flow_vars(analyzer, edge_flows, - dag.in_edges(sink)) - problem += pulp.lpSum(edge_flows_to_sink) == 1, "Flows to sink" - - # Add constraints for the exclusive path constraints. To ensure that - # the edges in each constraint are not taken together, the total flow - # through all_temp_files the edges should add to at least one unit less than - # the number of edges in the constraint. Hence, if a constraint - # contains edges e_a, e_b, e_c, then e_a + e_b + e_c must be less than 3. - # This way, all_temp_files three of these edges can never be taken together. - for constraint_num, path in enumerate(path_exclusive_constraints): - edge_flows_in_constraint = _get_edge_flow_vars(analyzer, edge_flows, path) - problem += (pulp.lpSum(edge_flows_in_constraint) <= (len(path) - 1), - "Path exclusive constraint %d" % (constraint_num + 1)) - - # Add constraints for the bundled path constraints. If a constraint - # contains edges e_a, e_b, e_c, e_d, and each edge *must* be taken, - # then e_b + e_c + e_d must sum up to e_a, scaled by -3 (or one less - # than the number of edges in the path constraint). Hence, the flow - # constraint is e_b + e_c + e_d = -3 * e_a. By default, we scale - # the first edge in a constraint with this negative value. - for constraint_num, path in enumerate(path_bundled_constraints): - first_edge = path[0] - first_edge_flow = _get_edge_flow_var(analyzer, edge_flows, first_edge) - edge_flows_for_rest = _get_edge_flow_vars(analyzer, edge_flows, path[1:]) - problem += \ - (pulp.lpSum(edge_flows_for_rest) == (len(path) - 1) * first_edge_flow, - "Path bundled constraint %d" % (constraint_num + 1)) - - # There may be bounds on the values of the paths that are generated - # by this function: we add constraints for these bounds. For this, - # we weight the PuLP variables for the edges using the list of - # edge weights provided, and then impose bounds on the sum. - weighted_edge_flow_vars = [] - for edge_index, edge_flow_var in edge_flows.items(): - edge_weight = edge_weights[edge_index] - weighted_edge_flow_vars.append(edge_weight * edge_flow_var) - interval = interval or Interval() - if interval.has_finite_lower_bound(): - problem += \ - (pulp.lpSum(weighted_edge_flow_vars) >= interval.lower_bound) - if interval.has_finite_upper_bound(): - problem += \ - (pulp.lpSum(weighted_edge_flow_vars) <= interval.upper_bound) - - logger.info("Variables created and constraints added.") - - logger.info("Constructing the objective function...") - # Finally, construct and add the objective function. - # We reuse the constraint (possibly) added in the last step of - # the constraint addition phase. - objective = pulp.lpSum(weighted_edge_flow_vars) - problem += objective - logger.info("Objective function constructed.") - - logger.info("Finding the maximum value of the objective function...") - - problem.sense = pulp.LpMaximize - problem_status = problem.solve(solver=get_ilp_solver(project_config.ilp_solver, project_config)) - if problem_status != pulp.LpStatusOptimal: - logger.info("Maximum value not found.") - return [], problem - obj_val_max = pulp.value(objective) - - logger.info("Maximum value found: %g" % obj_val_max) - - logger.info("Finding the path that corresponds to the maximum value...") - # Determine the edges along the extreme path using the solution. - - max_path = [edges[edge_num] for edge_num in edge_flows.keys() if (edge_flows[edge_num].value() == 1)] - logger.info("Path found.") - - logger.info("Finding the minimum value of the objective function...") - - problem.sense = pulp.LpMinimize - problem_status = problem.solve(solver=get_ilp_solver(project_config.ilp_solver, project_config)) - if problem_status != pulp.LpStatusOptimal: - logger.info("Minimum value not found.") - return [], problem - obj_val_min = pulp.value(objective) - - logger.info("Minimum value found: %g" % obj_val_min) - - logger.info("Finding the path that corresponds to the minimum value...") - # Determine the edges along the extreme path using the solution. - min_path = [edges[edge_num] for edge_num in edge_flows - if edge_flows[edge_num].value() == 1] - logger.info("Path found.") - - # Choose the correct extreme path based on the optimal solutions - # and the type of extreme path required. - abs_max, abs_min = abs(obj_val_max), abs(obj_val_min) - if extremum is Extremum.LONGEST: - extreme_path = max_path if abs_max >= abs_min else min_path - problem.sense = (pulp.LpMaximize if abs_max >= abs_min - else pulp.LpMinimize) - problem.obj_val = max(abs_max, abs_min) - elif extremum is Extremum.SHORTEST: - extreme_path = min_path if abs_max >= abs_min else max_path - problem.sense = (pulp.LpMinimize if abs_max >= abs_min - else pulp.LpMaximize) - problem.obj_val = min(abs_max, abs_min) - - # Arrange the nodes along the extreme path in order of traversal - # from source to sink. - result_path = [] - - logger.info("Arranging the nodes along the chosen extreme path " - "in order of traversal...") - # To do so, first construct a dictionary mapping a node along the path - # to the edge from that node. - extreme_path_dict = {} - for edge in extreme_path: - extreme_path_dict[edge[0]] = edge - # Now, "thread" a path through the dictionary. - curr_node = source - result_path.append(curr_node) - while curr_node in extreme_path_dict: - new_edge = extreme_path_dict[curr_node] - curr_node = new_edge[1] - result_path.append(curr_node) - logger.info("Nodes along the chosen extreme path arranged.") - - if project_config.debug_config.KEEP_ILP_SOLVER_OUTPUT: - _move_ilp_files(os.getcwd(), project_config.location_temp_dir) - else: - _remove_temp_ilp_files() - - # We're done! - return result_path, problem - - -def _move_ilp_files(source_dir, dest_dir): - """ - Moves the files that are generated when an integer linear program - is solved, from the source directory whose location is provided - to the destination directory whose location is provided. - - Parameters: - source_dir : - Location of the source directory - dest_dir : - Location of the destination directory - """ - move_files([_LP_NAME + r"-pulp\.lp", _LP_NAME + r"-pulp\.mps", - _LP_NAME + r"-pulp\.prt", _LP_NAME + r"-pulp\.sol"], - source_dir, dest_dir) - - -def _remove_temp_ilp_files(): - """ - Removes the temporary files that are generated when an - integer linear program is solved. - - """ - remove_files([r".*\.lp", r".*\.mps", r".*\.prt", r".*\.sol"], os.getcwd()) diff --git a/src/smt_solver/to_klee_format.py b/src/smt_solver/to_klee_format.py index 38978b3b..bc7fc0dc 100644 --- a/src/smt_solver/to_klee_format.py +++ b/src/smt_solver/to_klee_format.py @@ -17,7 +17,7 @@ def format_for_klee(c_file, c_file_path, c_file_gt_dir, n, total_number_of_label c_code = c_code.lstrip('\n') # Generate KLEE headers - klee_headers = "#include \n#include \n" + klee_headers = "#include \n#include \n" # Generate global boolean variables and initialize them to false/true global_booleans = "\n" diff --git a/src/unroller.py b/src/unroller.py deleted file mode 100644 index 386cc559..00000000 --- a/src/unroller.py +++ /dev/null @@ -1,121 +0,0 @@ -import os -import subprocess -import sys -import re - -def run_command(command, description): - """ - Run a shell command and handle errors. - """ - try: - print(f"Running: {description}") - subprocess.check_call(command, shell=True) - print(f"{description} completed successfully.\n") - except subprocess.CalledProcessError as e: - print(f"Error: {description} failed with exit code {e.returncode}.") - sys.exit(1) - -def compile_to_bitcode(c_file, output_bc): - """ - Compile the C file to LLVM bitcode (.bc file) using clang. - """ - command = f"clang -emit-llvm -Xclang -disable-O0-optnone -c {c_file} -o {output_bc}" - run_command(command, f"Compiling {c_file} to LLVM bitcode") - -def generate_llvm_ir(output_bc, output_ir): - """ - Generate LLVM Intermediate Representation (.ll file) from LLVM bitcode. - """ - command = f"llvm-dis {output_bc} -o {output_ir}" - run_command(command, f"Generating LLVM IR from bitcode {output_bc}") - -def unroll_llvm_ir(input_ir, output_ir): - """ - Generate LLVM Intermediate Representation (.ll file) from LLVM bitcode. - """ - command = f"opt -passes=loop-unroll -S {input_ir} -o {output_ir}" - run_command(command, f"Generating LLVM IR from {input_ir}") - -def generate_llvm_dag(output_bc): - """ - Generate LLVM DAG (.dot file) using opt. - """ - command = f"opt -passes=dot-cfg -S -disable-output {output_bc}" - run_command(command, f"Generating LLVM DAG from bitcode {output_bc}") - -def modify_loop_branches_to_next_block(input_file_path, output_file_path): - """ - Modifies an LLVM IR file to replace branches with `!llvm.loop` annotations - to point to the block that appears immediately after the block containing - the `!llvm.loop` expression. - - Args: - input_file_path (str): Path to the input LLVM IR file. - output_file_path (str): Path to save the modified LLVM IR file. - """ - with open(input_file_path, 'r') as file: - llvm_ir_code = file.read() - - # Define regex patterns for identifying branch instructions with !llvm.loop and block labels - block_pattern = re.compile(r'^(\d+):', re.MULTILINE) # Matches lines with block labels (e.g., "3:") - branch_with_loop_pattern = re.compile(r'br label %(\d+), !llvm.loop') - - # Find all blocks in the order they appear - blocks = [int(match.group(1)) for match in block_pattern.finditer(llvm_ir_code)] - - # Split the code into lines for processing - lines = llvm_ir_code.splitlines() - new_lines = [] - - # Iterate through each line, looking for `!llvm.loop` branches - current_block = None - for i, line in enumerate(lines): - # Check if the line starts a new block - block_match = block_pattern.match(line) - if block_match: - current_block = int(block_match.group(1)) - - # If a `!llvm.loop` branch is found, modify it to point to the next block - loop_branch_match = branch_with_loop_pattern.search(line) - if loop_branch_match and current_block is not None: - # Find the index of the current block in the blocks list - current_block_index = blocks.index(current_block) - # Ensure there is a next block after the current one - if current_block_index + 1 < len(blocks): - # Get the label of the next block - next_block_num = blocks[current_block_index + 1] - # Replace the branch target to point to this next block - line = line.replace(f'br label %{loop_branch_match.group(1)}', f'br label %{next_block_num}') - - # Append the modified or unmodified line to the result - new_lines.append(line) - - # Join the modified lines back together - modified_llvm_ir_code = "\n".join(new_lines) - - # Write the modified LLVM IR code to the output file - with open(output_file_path, 'w') as file: - file.write(modified_llvm_ir_code) - - print(f"Modified LLVM IR code saved to {output_file_path}") - -def assemble_bitcode(input_file, output_file): - run_command(f"llvm-as {input_file} -o {output_file}", "Assemble LLVM IR after unrolling") - - -def unroll(bc_filepath: str, output_file_folder: str, output_name: str): - - output_ir = f"{bc_filepath[:-3]}.ll" - unrolled_output_ir = f"{bc_filepath[:-3]}_unrolled.ll" - unrolled_mod_output_ir = f"{bc_filepath[:-3]}_unrolled_mod.ll" - unrolled_mod_output_bc = f"{bc_filepath[:-3]}_unrolled_mod.bc" - - generate_llvm_ir(bc_filepath, output_ir) - - unroll_llvm_ir(output_ir, unrolled_output_ir) - - modify_loop_branches_to_next_block(unrolled_output_ir, unrolled_mod_output_ir) - - assemble_bitcode(unrolled_mod_output_ir, unrolled_mod_output_bc) - - return unrolled_mod_output_bc \ No newline at end of file diff --git a/src/updateChecker.py b/src/updateChecker.py new file mode 100644 index 00000000..c69fed15 --- /dev/null +++ b/src/updateChecker.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python + +"""Exposes functions to determine if updates to GameTime are available.""" + +"""See the LICENSE file, located in the root directory of +the source distribution and +at http://verifun.eecs.berkeley.edu/gametime/about/LICENSE, +for details on the GameTime license and authors. +""" + + +import json +import urllib2 +import socket + +from setuptools.dist import pkg_resources +from pkg_resources import parse_version + +from defaults import config, logger + + +def getLatestVersionInfo(): + """Obtains information about the latest available version of GameTime. + + Returns: + Dictionary that contains information about the latest available + version of GameTime. If the dictionary is empty, then no such + information could be obtained. + + If, however, the dictionary is not empty, it has at least + the two following keys: + - `version`, whose corresponding value is the number of + the latest version of GameTime, and + - `info_url`, whose corresponding value is the URL of + the webpage that provides more information about + the latest version of GameTime. + """ + try: + logger.info("Retrieving data about the latest version of GameTime...") + urlHandler = urllib2.urlopen(config.LATEST_VERSION_INFO_URL, timeout=3) + + latestVersionInfo = json.load(urlHandler) + logger.info("Data retrieved.") + + if not latestVersionInfo.has_key("version"): + raise ValueError("Data retrieved does not have the number of " + "the latest version of GameTime.") + elif not latestVersionInfo.has_key("info_url"): + raise ValueError("Data retrieved does not have URL of the webpage " + "that provides more information about " + "the latest version of GameTime.") + return latestVersionInfo + except urllib2.URLError as e: + errorReason = e.reason + # Special case + if len(e.args) > 0 and e.args[0].errno == 11001: + errorReason = "No Internet connection detected." + logger.warning("Unable to retrieve data about the latest " + "version of GameTime: %s" % errorReason) + return {} + except socket.timeout as e: + logger.warning("Unable to retrieve data about the latest " + "version of GameTime: The attempt to connect with " + "the URL that provides this data timed out.") + return {} + except ValueError as e: + logger.warning("Data retrieved was in an incorrect format: %s" % e) + return {} + +def isUpdateAvailable(): + """Checks if an update to the current version of GameTime is available. + + Returns: + Tuple of two elements: The first element is `True` if an update + is available and `False` otherwise. The second element is a + dictionary that contains information about the latest version of + GameTime that is available. If the dictionary is empty, then + no information about the latest available version of GameTime + could be obtained. + + If non-empty, the dictionary has at least the two following keys: + - `version`, whose corresponding value is the number of + the newer version of GameTime, and + - `info_url`, whose corresponding value is the URL of + the webpage that provides more information about + the newer version of GameTime. + """ + latestVersionInfo = getLatestVersionInfo() + if latestVersionInfo: + return ((parse_version(latestVersionInfo["version"]) > + parse_version(config.VERSION)), + latestVersionInfo) + return (False, {}) diff --git a/test/tacle_test/programs/prime/prime.c b/test/tacle_test/programs/prime/prime.c index abf7dc4a..8b3ddff5 100644 --- a/test/tacle_test/programs/prime/prime.c +++ b/test/tacle_test/programs/prime/prime.c @@ -4,7 +4,9 @@ Forward declaration of functions */ - +#include +#include +#include int prime_prime ( int n ) { @@ -12,10 +14,16 @@ int prime_prime ( int n ) if ( n % 2 ) return ( n == 2 ); // _Pragma( "loopbound min 0 max 16" ) - #pragma unroll 16 - for ( i = 3; i * i <= 357; i += 2 ) { + #pragma unroll 5 + // #pragma clang loop unroll(full) + for ( i = 3; i * i <= 3; i += 2 ) { if ( n % i ) /* ai: loop here min 0 max 357 end; */ return 0; + // n+=1; } -} \ No newline at end of file + return n; + +} + + diff --git a/test/tacle_test/wcet_test.py b/test/tacle_test/wcet_test.py index 9d9aa04d..0c0453fc 100644 --- a/test/tacle_test/wcet_test.py +++ b/test/tacle_test/wcet_test.py @@ -36,6 +36,7 @@ def create_analyzer(self): # print(p.get_measured_value()) def test_wcet_analyzer(self): + # do all of the preprocessing analyzer = self.create_analyzer() analyzer.create_dag() @@ -133,17 +134,23 @@ class TestInsertSortARM(TestARMBackend): suite = unittest.TestSuite() #Programs - suite.addTests(loader.loadTestsFromTestCase(TestIfElifElseFlexpret)) + # suite.addTests(loader.loadTestsFromTestCase(TestIfElifElseFlexpret)) # suite.addTests(loader.loadTestsFromTestCase(TestBitcnt2Flexpret)) # suite.addTests(loader.loadTestsFromTestCase(TestPrimeFlexpret)) # suite.addTests(loader.loadTestsFromTestCase(TestIfElifElseX86)) # suite.addTests(loader.loadTestsFromTestCase(TestBinarysearchARM)) # suite.addTests(loader.loadTestsFromTestCase(TestIfElifElseARM)) # suite.addTests(loader.loadTestsFromTestCase(TestBitcnt2ARM)) - # suite.addTests(loader.loadTestsFromTestCase(TestPrimeARM)) + suite.addTests(loader.loadTestsFromTestCase(TestIfElifElseFlexpret)) # suite.addTests(loader.loadTestsFromTestCase(TestCountNegativeARM)) - suite.addTests(loader.loadTestsFromTestCase(TestModexpARM)) + # suite.addTests(loader.loadTestsFromTestCase(TestIfElifElseFlexpret)) # suite.addTests(loader.loadTestsFromTestCase(TestInsertSortARM)) runner = unittest.TextTestRunner() runner.run(suite) +# nano ~/.bashrc +# sbt test +# source env.bash +# cmake -B build && cd build +# make all install +# make && ctest \ No newline at end of file