Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prototype a new CLI #904

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions amaranth/back/rtlil.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 16 additions & 4 deletions amaranth/lib/fifo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -64,7 +65,7 @@
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))
Expand All @@ -85,6 +86,17 @@
self.r_en = Signal()
self.r_level = Signal(range(depth + 1))

@property
def signature(self):
return Signature({

Check warning on line 91 in amaranth/lib/fifo.py

View check run for this annotation

Codecov / codecov/patch

amaranth/lib/fifo.py#L91

Added line #L91 was not covered by tests
"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):
Expand Down Expand Up @@ -116,7 +128,7 @@
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))
Expand Down Expand Up @@ -220,7 +232,7 @@
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))
Expand Down Expand Up @@ -295,7 +307,7 @@
""".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)
Expand Down
130 changes: 130 additions & 0 deletions amaranth_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It took me a while to realize that by "object reference", the error means "your input argument fails to match a regex", rather than "Python couldn't find an object with that name" (which is what the ImportError exception handler is for).

Perhaps this else branch could have "expected format is path.to.mod:Obj" or something?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep this could be improved!!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Care to submit a PR against a PR?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do in the coming days (my response time will be sporadic until possibly this Monday afternoon).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR is done: whitequark#1


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)
whitequark marked this conversation as resolved.
Show resolved Hide resolved
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")
10 changes: 4 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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

Expand Down