From 6c8949cacc4cb4970e45265646b992571e788a8d Mon Sep 17 00:00:00 2001 From: Krystine Sherwin <93062060+KrystalDelusion@users.noreply.github.com> Date: Wed, 24 Jan 2024 09:56:00 +1300 Subject: [PATCH] Docs: static opt macro list Also adds `docs/tests/macro_commands.py` which checks all commands in `code_examples/macro_commands` against the current yosys build. Format similar to `run-test.sh` files: logging the file under test and reporting errors. --- .../code_examples/macro_commands/opt.ys | 14 +++ docs/source/using_yosys/synthesis/opt.rst | 6 +- docs/tests/macro_commands.py | 91 +++++++++++++++++++ 3 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 docs/source/code_examples/macro_commands/opt.ys create mode 100755 docs/tests/macro_commands.py diff --git a/docs/source/code_examples/macro_commands/opt.ys b/docs/source/code_examples/macro_commands/opt.ys new file mode 100644 index 000000000..cb883bc58 --- /dev/null +++ b/docs/source/code_examples/macro_commands/opt.ys @@ -0,0 +1,14 @@ +#start: passes in the following order: +#end: When called with -fast +opt_expr +opt_merge -nomux + +do + opt_muxtree + opt_reduce + opt_merge + opt_share (-full only) + opt_dff (except when called with -noff) + opt_clean + opt_expr +while diff --git a/docs/source/using_yosys/synthesis/opt.rst b/docs/source/using_yosys/synthesis/opt.rst index 0ba108131..7861f66d4 100644 --- a/docs/source/using_yosys/synthesis/opt.rst +++ b/docs/source/using_yosys/synthesis/opt.rst @@ -14,11 +14,9 @@ includes removing unused signals and cells and const folding. It is recommended to run this pass after each major step in the synthesis script. As listed in :doc:`/cmd/opt`, this macro command calls the following ``opt_*`` commands: -.. literalinclude:: /cmd/opt.rst +.. literalinclude:: /code_examples/macro_commands/opt.ys :language: yoscrypt - :start-after: following order: - :end-at: while - :dedent: + :start-after: #end: :caption: Passes called by :cmd:ref:`opt` .. _adv_opt_expr: diff --git a/docs/tests/macro_commands.py b/docs/tests/macro_commands.py new file mode 100755 index 000000000..7942e4d31 --- /dev/null +++ b/docs/tests/macro_commands.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 + +import logging +from pathlib import Path +import re +import subprocess +import sys + +# basic logging setup +logging.basicConfig(level=logging.INFO) + +# expects __file__ = yosys/docs/tests/macro_commands.py +TESTS_DIR = Path(__file__).parent +ROOT_DIR = TESTS_DIR.parent.parent +THIS_FILE = (TESTS_DIR / "macro_commands.py").relative_to(ROOT_DIR) +MACRO_SOURCE = TESTS_DIR.parent / "source" / "code_examples" / "macro_commands" +assert MACRO_SOURCE.exists(), f"can't find macro_commands in {MACRO_SOURCE}" + +YOSYS = TESTS_DIR.parent.parent / "yosys" +assert YOSYS.exists(), f"can't find yosys executable in {YOSYS}" + +raise_error = False +# get all macro commands being used +for macro in MACRO_SOURCE.glob("*.ys"): + # log current test + relative_path = macro.relative_to(ROOT_DIR) + logging.log(logging.INFO, f"Checking {relative_path}") + # files in macros_commands should be named {command}.ys + command = macro.stem + # run `yosys -h {command}` to capture current sub-commands + proc = subprocess.run([YOSYS, "-QTh", command], capture_output=True, text=True) + with open(macro) as f: + # {command.ys} starts with two commented lines, the first is the text + # immediately prior to the macro body. The second is the text + # immediately after. + start = f.readline() + end = f.readline() + expected_content = f.readlines() + # parse {command.ys} + if "#start:" not in start or "#end:" not in end: + logging.error(f"Missing start and/or end string in {relative_path}, see {THIS_FILE}") + raise_error = True + continue + start = start.replace("#start:", "").strip() + end = end.replace("#end:", "").strip() + # attempt to find macro body in help output + match = re.fullmatch(f".*{start}(.*){end}.*", proc.stdout, re.DOTALL) + if not match: + logging.error(f"Couldn't find {start!r} and/or {end!r} in `yosys -h {command}` output") + raise_error = True + continue + actual_content = match.group(1).strip().splitlines() + # iterate over and compare expected v actual + for (expected, actual) in zip(expected_content, actual_content): + expected = expected.strip() + actual = actual.strip() + # raw match + if expected == actual: + continue + + # rip apart formatting to match line parts + pattern = r"(?P\S+)(?P \[.*\])?(?P.*?)(?P \(.*\))?(?P\s+#.*)?" + try: + expected_dict = re.fullmatch(pattern, expected).groupdict() + except AttributeError: + logging.error(f"Bad formatting in {relative_path}: {expected!r}") + raise_error = True + continue + + try: + actual_dict = re.fullmatch(pattern, actual).groupdict() + except AttributeError: + logging.error(f"Unexpected formatting in `yosys -h {command}` output, {actual!r}") + raise_error = True + continue + + does_match = expected_dict["cmd"] == actual_dict["cmd"] + + #todo: check expected_dict["pass"] is a subset of actual_dict["pass"] + # only check opt and cond match if they're in {command}.ys + match_keys = ["opt", "cond"] + for key in match_keys: + if expected_dict[key] and expected_dict[key] != actual_dict[key]: + does_match = False + + # raise error on mismatch + if not does_match: + logging.error(f"Expected {expected!r}, got {actual!r}") + raise_error = True + +sys.exit(raise_error)