From 0e6378b0030313d04f90e1bba4c5bb6c111c35e0 Mon Sep 17 00:00:00 2001 From: Catherine Date: Tue, 5 Sep 2023 21:35:34 +0000 Subject: [PATCH 1/4] amaranth._cli: prototype. (WIP) --- amaranth_cli/__init__.py | 130 +++++++++++++++++++++++++++++++++++++++ pyproject.toml | 10 ++- 2 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 amaranth_cli/__init__.py diff --git a/amaranth_cli/__init__.py b/amaranth_cli/__init__.py new file mode 100644 index 000000000..70fd51eae --- /dev/null +++ b/amaranth_cli/__init__.py @@ -0,0 +1,130 @@ +""" +This file is not a part of the Amaranth module tree because the CLI needs to emit Make-style +dependency files as a part of the generation process. In order for `from amaranth import *` +to work as a prelude, it has to load several of the files under `amaranth/`, which means +these will not be loaded later in the process, and not recorded as dependencies. +""" + +import importlib +import argparse +import stat +import sys +import os +import re + + +def _build_parser(): + def component(reference): + from amaranth import Elaboratable + + if m := re.match(r"(\w+(?:\.\w+)*):(\w+(?:\.\w+)*)", reference, re.IGNORECASE|re.ASCII): + mod_name, qual_name = m[1], m[2] + try: + obj = importlib.import_module(mod_name) + except ImportError as e: + raise argparse.ArgumentTypeError(f"{mod_name!r} does not refer to " + "an importable Python module") from e + try: + for attr in qual_name.split("."): + obj = getattr(obj, attr) + except AttributeError as e: + raise argparse.ArgumentTypeError(f"{qual_name!r} does not refer to an object " + f"within the {mod_name!r} module") from e + if not issubclass(obj, Elaboratable): + raise argparse.ArgumentTypeError(f"'{qual_name}:{mod_name}' refers to an object that is not elaboratable") + return obj + else: + raise argparse.ArgumentTypeError(f"{reference!r} is not a Python object reference") + + parser = argparse.ArgumentParser( + "amaranth", description=""" + Amaranth HDL command line interface. + """) + operation = parser.add_subparsers( + metavar="OPERATION", help="operation to perform", + dest="operation", required=True) + + op_generate = operation.add_parser( + "generate", help="generate code in a different language from Amaranth code", + aliases=("gen", "g")) + op_generate.add_argument( + metavar="COMPONENT", help="Amaranth component to convert, e.g. `pkg.mod:Cls`", + dest="component", type=component) + op_generate.add_argument( + "-n", "--name", metavar="NAME", help="name of the toplevel module, also prefixed to others", + dest="name", type=str, default=None) + op_generate.add_argument( + "-p", "--param", metavar=("NAME", "VALUE"), help="parameter(s) for the component", + dest="params", nargs=2, type=str, action="append", default=[]) + gen_language = op_generate.add_subparsers( + metavar="LANGUAGE", help="language to generate code in", + dest="language", required=True) + + lang_verilog = gen_language.add_parser( + "verilog", help="generate Verilog code") + lang_verilog.add_argument( + "-v", metavar="VERILOG-FILE", help="Verilog file to write", + dest="verilog_file", type=argparse.FileType("w")) + lang_verilog.add_argument( + "-d", metavar="DEP-FILE", help="Make-style dependency file to write", + dest="dep_file", type=argparse.FileType("w")) + + return parser + + +def main(args=None): + # Hook the `open()` function to find out which files are being opened by Amaranth code. + files_being_opened = set() + special_file_opened = False + def dep_audit_hook(event, args): + nonlocal special_file_opened + if files_being_opened is not None and event == "open": + filename, mode, flags = args + if mode is None or "r" in mode or "+" in mode: + if isinstance(filename, bytes): + filename = filename.decode("utf-8") + if isinstance(filename, str) and stat.S_ISREG(os.stat(filename).st_mode): + files_being_opened.add(filename) + else: + special_file_opened = True + sys.addaudithook(dep_audit_hook) + + # Parse arguments and instantiate components + args = _build_parser().parse_args(args) + if args.operation in ("generate", "gen", "g"): + params = dict(args.params) + params = {name: cls(params[name]) + for name, cls in args.component.__init__.__annotations__.items()} + component = args.component(**params) + + # Capture the set of opened files, as well as the loaded Python modules. + files_opened, files_being_opened = files_being_opened, None + modules_after = list(sys.modules.values()) + + # Remove *.pyc files from the set of open files and replace them with their *.py equivalents. + dep_files = set() + dep_files.update(files_opened) + for module in modules_after: + if getattr(module, "__spec__", None) is None: + continue + if module.__spec__.cached in dep_files: + dep_files.discard(module.__spec__.cached) + dep_files.add(module.__spec__.origin) + + if args.operation in ("generate", "gen", "g"): + if args.language == "verilog": + # Generate Verilog file with `-v` or without arguments. + if args.verilog_file or not (args.verilog_file or args.dep_file): + from amaranth.back.verilog import convert + code = convert(component, name=(args.name or args.component.__name__),) + (args.verilog_file or sys.stdout).write(code) + + # Generate dependency file with `-d`. + if args.verilog_file and args.dep_file: + args.dep_file.write(f"{args.verilog_file.name}:") + if not special_file_opened: + for file in sorted(dep_files): + args.dep_file.write(f" \\\n {file}") + args.dep_file.write("\n") + else: + args.dep_file.write(f"\n.PHONY: {args.verilog_file.name}\n") diff --git a/pyproject.toml b/pyproject.toml index 8e07f7f83..0dc9d8182 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,11 +19,12 @@ dependencies = [ ] [project.optional-dependencies] -# this version requirement needs to be synchronized with the one in amaranth.back.verilog! +# This version requirement needs to be synchronized with the one in amaranth.back.verilog! builtin-yosys = ["amaranth-yosys>=0.10"] remote-build = ["paramiko~=2.7"] [project.scripts] +amaranth = "amaranth_cli:main" amaranth-rpc = "amaranth.rpc:main" [project.urls] @@ -39,11 +40,8 @@ requires = ["pdm-backend"] build-backend = "pdm.backend" [tool.pdm.build] -# If amaranth 0.3 is checked out with git (e.g. as a part of a persistent editable install or -# a git worktree cached by tools like poetry), it can have an empty `nmigen` directory left over, -# which causes a hard error because setuptools cannot determine the top-level package. -# Add a workaround to improve experience for people upgrading from old checkouts. -includes = ["amaranth/"] +# The docstring in `amaranth_cli/__init__.py` explains why it is not under `amaranth/`. +packages = ["amaranth", "amaranth_cli"] # Development workflow configuration From 797cffd7ec14e60474c468b6453e5876e31732ee Mon Sep 17 00:00:00 2001 From: Catherine Date: Tue, 5 Sep 2023 22:26:52 +0000 Subject: [PATCH 2/4] lib.fifo: annotate for use with CLI. (WIP) --- amaranth/lib/fifo.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/amaranth/lib/fifo.py b/amaranth/lib/fifo.py index 4fd6f6ccc..5df42241e 100644 --- a/amaranth/lib/fifo.py +++ b/amaranth/lib/fifo.py @@ -3,6 +3,7 @@ from .. import * from ..asserts import * from .._utils import log2_int +from .wiring import Signature, In, Out from .coding import GrayEncoder, GrayDecoder from .cdc import FFSynchronizer, AsyncFFSynchronizer @@ -64,7 +65,7 @@ class FIFOInterface: w_attributes="", r_attributes="") - def __init__(self, *, width, depth, fwft): + def __init__(self, *, width: int, depth: int, fwft): if not isinstance(width, int) or width < 0: raise TypeError("FIFO width must be a non-negative integer, not {!r}" .format(width)) @@ -85,6 +86,17 @@ def __init__(self, *, width, depth, fwft): self.r_en = Signal() self.r_level = Signal(range(depth + 1)) + @property + def signature(self): + return Signature({ + "w_data": In(self.width), + "w_rdy": Out(1), + "w_en": In(1), + "r_data": Out(self.width), + "r_rdy": Out(1), + "w_en": In(1), + }) + def _incr(signal, modulo): if modulo == 2 ** len(signal): @@ -116,7 +128,7 @@ class SyncFIFO(Elaboratable, FIFOInterface): r_attributes="", w_attributes="") - def __init__(self, *, width, depth, fwft=True): + def __init__(self, *, width: int, depth: int, fwft=True): super().__init__(width=width, depth=depth, fwft=fwft) self.level = Signal(range(depth + 1)) @@ -220,7 +232,7 @@ class SyncFIFOBuffered(Elaboratable, FIFOInterface): r_attributes="", w_attributes="") - def __init__(self, *, width, depth): + def __init__(self, *, width: int, depth: int): super().__init__(width=width, depth=depth, fwft=True) self.level = Signal(range(depth + 1)) @@ -295,7 +307,7 @@ class AsyncFIFO(Elaboratable, FIFOInterface): """.strip(), w_attributes="") - def __init__(self, *, width, depth, r_domain="read", w_domain="write", exact_depth=False): + def __init__(self, *, width: int, depth: int, r_domain="read", w_domain="write", exact_depth=False): if depth != 0: try: depth_bits = log2_int(depth, need_pow2=exact_depth) From 7ff2e44329a5e9cf2e153112e80a267d456d9ea3 Mon Sep 17 00:00:00 2001 From: Catherine Date: Tue, 12 Sep 2023 01:54:11 +0000 Subject: [PATCH 3/4] back.rtlil: put hierarchy in module name instead of an attribute. The attribute sees essentially no use and the information is much better served by putting it in the module name. In addition this means that the entire tree can be renamed simply by renaming the top module. Tools like GTKWave show the names of the instances, not the modules, so they are not affected by the longer names. --- amaranth/back/rtlil.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/amaranth/back/rtlil.py b/amaranth/back/rtlil.py index 78a8073ef..52fa91f07 100644 --- a/amaranth/back/rtlil.py +++ b/amaranth/back/rtlil.py @@ -820,11 +820,10 @@ def _convert_fragment(builder, fragment, name_map, hierarchy): else: return "\\{}".format(fragment.type), port_map - module_name = hierarchy[-1] or "anonymous" + module_name = ".".join(name or "anonymous" for name in hierarchy) module_attrs = OrderedDict() if len(hierarchy) == 1: module_attrs["top"] = 1 - module_attrs["amaranth.hierarchy"] = ".".join(name or "anonymous" for name in hierarchy) with builder.module(module_name, attrs=module_attrs) as module: compiler_state = _ValueCompilerState(module) From 1fd9388f616b08dba5b9f60c5767cdada53efb0b Mon Sep 17 00:00:00 2001 From: "William D. Jones" Date: Mon, 2 Oct 2023 21:42:22 -0400 Subject: [PATCH 4/4] Improve Amaranth CLI message when given component name fails to match regex. --- amaranth_cli/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/amaranth_cli/__init__.py b/amaranth_cli/__init__.py index 70fd51eae..eacc80ae1 100644 --- a/amaranth_cli/__init__.py +++ b/amaranth_cli/__init__.py @@ -34,7 +34,8 @@ def component(reference): raise argparse.ArgumentTypeError(f"'{qual_name}:{mod_name}' refers to an object that is not elaboratable") return obj else: - raise argparse.ArgumentTypeError(f"{reference!r} is not a Python object reference") + raise argparse.ArgumentTypeError(f"{reference!r} can not be parsed as a Python object reference, " + "expecting a name like: 'path.to.module:ObjectInModule'") parser = argparse.ArgumentParser( "amaranth", description="""