[formal/script] Update generic formal flow naming from `fpv` to `formal`
This PR updates formal flow names. In current repo, all formal related
generic flow is called `fpv` instead of `flow`.
But as Sri suggested in PR #5770, the generic flow name should be
`formal`, and under formal we can have sub-flows such as `fpv`, `conn`,
`spv`, etc.
Signed-off-by: Cindy Chen <chencindy@google.com>
diff --git a/util/dvsim/FormalCfg.py b/util/dvsim/FormalCfg.py
new file mode 100644
index 0000000..2a80fa6
--- /dev/null
+++ b/util/dvsim/FormalCfg.py
@@ -0,0 +1,285 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+import logging as log
+from pathlib import Path
+
+import hjson
+from tabulate import tabulate
+
+from OneShotCfg import OneShotCfg
+from utils import VERBOSE, subst_wildcards
+
+
+class FormalCfg(OneShotCfg):
+ """Derivative class for running formal tools.
+ """
+
+ flow = 'formal'
+
+ def __init__(self, flow_cfg_file, hjson_data, args, mk_config):
+ super().__init__(flow_cfg_file, hjson_data, args, mk_config)
+ self.header = ["name", "errors", "warnings", "proven", "cex", "undetermined",
+ "covered", "unreachable", "pass_rate", "cov_rate"]
+ self.summary_header = ["name", "pass_rate", "stimuli_cov", "coi_cov", "prove_cov"]
+ self.results_title = self.name.upper() + " Formal Results"
+
+ def parse_dict_to_str(self, input_dict, excl_keys = []):
+ # This is a helper function to parse dictionary items into a string.
+ # This function has an optional input "excl_keys" for user to exclude
+ # printing out certain items according to their keys.
+ # Note this function did not sort the input dictionary's key value
+ # before printing the keys and items. If input dictionary is not an
+ # OrderedDictionary, print out key order is not predictable.
+ # This function works for Hjson lib outputs because the lib uses an
+ # OrderDict when it reads dictionaries.
+ # Example Input:
+ # {
+ # "unreachable": ["prop1, prop2, prop3"],
+ # "cex" : ["prop1"],
+ # }
+ # Example Output:
+ # string = "unreachable:
+ # ```
+ # prop1
+ # prop2
+ # prop3
+ # ```
+ # cex:
+ # ```
+ # prop1
+ # ```"
+ output_str = ""
+ for key, item in input_dict.items():
+ if (key not in excl_keys) and item:
+ output_str += "\n" + key + ":\n"
+ output_str += "```\n"
+ output_str += "\n".join(item)
+ output_str += "\n```\n"
+ return output_str
+
+ def get_summary(self, result):
+ summary = []
+ formal_summary = result.get("summary")
+ if formal_summary is None:
+ results_str = "No summary information found\n"
+ summary.append("N/A")
+ else:
+ colalign = ("center", ) * len(self.header)
+ table = [self.header]
+ table.append([
+ self.name,
+ str(formal_summary["errors"]) + " E ",
+ str(formal_summary["warnings"]) + " W ",
+ str(formal_summary["proven"]) + " G ",
+ str(formal_summary["cex"]) + " E ",
+ str(formal_summary["undetermined"]) + " W ",
+ str(formal_summary["covered"]) + " G ",
+ str(formal_summary["unreachable"]) + " E ",
+ formal_summary["pass_rate"],
+ formal_summary["cov_rate"]
+ ])
+ summary.append(formal_summary["pass_rate"])
+ if len(table) > 1:
+ results_str = tabulate(table, headers="firstrow", tablefmt="pipe",
+ colalign=colalign)
+ else:
+ results_str = "No content in summary\n"
+ summary.append("N/A")
+ return results_str, summary
+
+ def get_coverage(self, result):
+ summary = []
+ formal_coverage = result.get("coverage")
+ if formal_coverage is None:
+ results_str = "No coverage information found\n"
+ summary = ["N/A", "N/A", "N/A"]
+ else:
+ cov_header = ["stimuli", "coi", "proof"]
+ cov_colalign = ("center", ) * len(cov_header)
+ cov_table = [cov_header]
+ cov_table.append([
+ formal_coverage["stimuli"],
+ formal_coverage["coi"],
+ formal_coverage["proof"]
+ ])
+ summary.append(formal_coverage["stimuli"])
+ summary.append(formal_coverage["coi"])
+ summary.append(formal_coverage["proof"])
+
+ if len(cov_table) > 1:
+ results_str = tabulate(cov_table, headers="firstrow",
+ tablefmt="pipe", colalign=cov_colalign)
+
+ else:
+ results_str = "No content in formal_coverage\n"
+ summary = ["N/A", "N/A", "N/A"]
+ return results_str, summary
+
+ def gen_results_summary(self):
+ # Gathers the aggregated results from all sub configs
+ # The results_summary will only contain the passing rate and
+ # percentages of the stimuli, coi, and proven coverage
+ # The email_summary will contain all the information from results_md
+ log.info("Create a result summary")
+
+ results_str = "## " + self.results_title + " (Summary)\n\n"
+ results_str += "### " + self.timestamp_long + "\n"
+ if self.revision:
+ results_str += "### " + self.revision + "\n"
+ results_str += "### Branch: " + self.branch + "\n"
+ results_str += "\n"
+
+ colalign = ("center", ) * len(self.summary_header)
+ table = [self.summary_header]
+ for cfg in self.cfgs:
+ try:
+ table.append(cfg.result_summary[cfg.name])
+ except KeyError as e:
+ table.append([cfg.name, "ERROR", "N/A", "N/A", "N/A"])
+ log.error("cfg: %s could not find generated results_summary: %s", cfg.name, e)
+ if len(table) > 1:
+ self.results_summary_md = results_str + tabulate(
+ table, headers="firstrow", tablefmt="pipe", colalign=colalign)
+ else:
+ self.results_summary_md = results_str
+
+ log.info("[result summary]: %s", self.results_summary_md)
+
+ # Generate email results summary
+ colalign = ("left", ) + ("center", ) * (len(self.header) - 1)
+ email_table = [self.header]
+ error_message = ""
+
+ for cfg in self.cfgs:
+ email_result = cfg.result.get("summary")
+ if email_result is not None:
+ email_table.append([
+ cfg.name,
+ str(email_result["errors"]) + " E ",
+ str(email_result["warnings"]) + " W ",
+ str(email_result["proven"]) + " G ",
+ str(email_result["cex"]) + " E ",
+ str(email_result["undetermined"]) + " W ",
+ str(email_result["covered"]) + " G ",
+ str(email_result["unreachable"]) + " E ",
+ email_result["pass_rate"],
+ email_result["cov_rate"]
+ ])
+ messages = cfg.result.get("messages")
+ if messages is not None:
+ # TODO: temp disable printing out warnings in results_summary
+ # Will clean up formal warnings first, then display warnings
+ error = self.parse_dict_to_str(messages, ["warnings"])
+ if error:
+ error_message += "\n#### " + cfg.name + "\n"
+ error_message += error
+
+ if len(email_table) > 1:
+ self.email_summary_md = results_str + tabulate(
+ email_table, headers="firstrow", tablefmt="pipe", colalign=colalign)
+ self.email_summary_md += error_message
+
+ return self.results_summary_md
+
+ def _gen_results(self, results):
+ # This function is called after the regression and looks for
+ # results.hjson file with aggregated results from the formal logfile.
+ # The hjson file is required to follow this format:
+ # {
+ # "messages": {
+ # "errors" : []
+ # "warnings" : []
+ # "cex" : ["property1", "property2"...],
+ # "undetermined": [],
+ # "unreachable" : [],
+ # },
+ #
+ # "summary": {
+ # "errors" : 0
+ # "warnings" : 2
+ # "proven" : 20,
+ # "cex" : 5,
+ # "covered" : 18,
+ # "undetermined": 7,
+ # "unreachable" : 2,
+ # "pass_rate" : "90 %",
+ # "cover_rate" : "90 %"
+ # },
+ # }
+ # The categories for property results are: proven, cex, undetermined,
+ # covered, and unreachable.
+ #
+ # If coverage was enabled then results.hjson will also have an item that
+ # shows formal coverage. It will have the following format:
+ # "coverage": {
+ # stimuli: "90 %",
+ # coi : "90 %",
+ # proof : "80 %"
+ # }
+ results_str = "## " + self.results_title + "\n\n"
+ results_str += "### " + self.timestamp_long + "\n"
+ if self.revision:
+ results_str += "### " + self.revision + "\n"
+ results_str += "### Branch: " + self.branch + "\n"
+ results_str += "### Formal Tool: " + self.tool.upper() + "\n"
+ results_str += "### LogFile dir: " + self.scratch_path + "/default\n\n"
+
+ summary = [self.name] # cfg summary for publish results
+
+ if len(self.build_modes) != 1:
+ mode_names = [mode.name for mode in self.build_modes]
+ log.error("Formal only supports mode 'default', but found these modes: %s", mode_names)
+ else:
+ mode = self.build_modes[0]
+ result_data = Path(subst_wildcards(self.build_dir, {"build_mode": mode.name}) +
+ '/results.hjson')
+ try:
+ with open(result_data, "r") as results_file:
+ self.result = hjson.load(results_file, use_decimal=True)
+ except IOError as err:
+ log.warning("%s", err)
+ self.result = {
+ "messages": {
+ "errors": ["IOError: %s" % err],
+ }
+ }
+
+ formal_result_str, formal_summary = self.get_summary(self.result)
+ results_str += formal_result_str
+ summary += formal_summary
+
+ if self.cov:
+ results_str += "\n\n## Coverage Results\n"
+ results_str += ("### Coverage html file dir: " +
+ self.scratch_path + "/default/formal-icarus\n\n")
+ cov_result_str, cov_summary = self.get_coverage(self.result)
+ results_str += cov_result_str
+ summary += cov_summary
+
+ messages = self.result.get("messages")
+ if messages is not None:
+ results_str += self.parse_dict_to_str(messages)
+
+ # Write results to the scratch area
+ self.results_md = results_str
+ results_file = self.scratch_path + "/results_" + self.timestamp + ".md"
+ with open(results_file, 'w') as f:
+ f.write(self.results_md)
+
+ # Generate result summary
+ if not self.cov:
+ summary += ["N/A", "N/A", "N/A"]
+ self.result_summary[self.name] = summary
+
+ log.log(VERBOSE, "[results page]: [%s] [%s]", self.name, results_file)
+ return self.results_md
+
+ def _publish_results(self):
+ '''This does nothing: detailed messages from the logfile are not published
+
+ Our agreement with tool vendors allows us to publish the summary
+ results (as in gen_results_summary).
+ '''
+ return