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) 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) diff --git a/amaranth_cli/__init__.py b/amaranth_cli/__init__.py new file mode 100644 index 000000000..eacc80ae1 --- /dev/null +++ b/amaranth_cli/__init__.py @@ -0,0 +1,131 @@ +""" +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} can not be parsed as a Python object reference, " + "expecting a name like: 'path.to.module:ObjectInModule'") + + 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