From c7ae8e9a18b57d8cad4b6c1e53045774cd76a512 Mon Sep 17 00:00:00 2001 From: Catherine <whitequark@whitequark.org> Date: Tue, 22 Aug 2023 12:17:09 +1000 Subject: [PATCH 1/2] [WIP] hdl.ast: add Display statement, a mixture of print() and format(). Rebase of dc6a805dc3085557e698b8f28b36d029a9cd3709. --- amaranth/__init__.py | 1 + amaranth/back/rtlil.py | 3 ++ amaranth/hdl/__init__.py | 2 + amaranth/hdl/ast.py | 81 ++++++++++++++++++++++++++++++++++++++-- amaranth/hdl/dsl.py | 5 ++- amaranth/hdl/xfrm.py | 40 ++++++++++++++------ amaranth/sim/_pyrtl.py | 43 ++++++++++++--------- tests/test_hdl_dsl.py | 3 +- 8 files changed, 142 insertions(+), 36 deletions(-) diff --git a/amaranth/__init__.py b/amaranth/__init__.py index 95a6cce20..ffb16ca0e 100644 --- a/amaranth/__init__.py +++ b/amaranth/__init__.py @@ -16,6 +16,7 @@ __all__ = [ "Shape", "unsigned", "signed", "Value", "Const", "C", "Mux", "Cat", "Repl", "Array", "Signal", "ClockSignal", "ResetSignal", + "Display", "Module", "ClockDomain", "Elaboratable", "Fragment", "Instance", diff --git a/amaranth/back/rtlil.py b/amaranth/back/rtlil.py index 52fa91f07..e6e968588 100644 --- a/amaranth/back/rtlil.py +++ b/amaranth/back/rtlil.py @@ -737,6 +737,9 @@ def on_Assign(self, stmt): else: self._case.assign(self.lhs_compiler(stmt.lhs), rhs_sigspec) + def on_Display(self, stmt): + raise NotImplementedError + def on_property(self, stmt): self(stmt._check.eq(stmt.test)) self(stmt._en.eq(1)) diff --git a/amaranth/hdl/__init__.py b/amaranth/hdl/__init__.py index 1e506d0ba..d65a3bbb0 100644 --- a/amaranth/hdl/__init__.py +++ b/amaranth/hdl/__init__.py @@ -2,6 +2,7 @@ from .ast import Shape, unsigned, signed from .ast import Value, Const, C, Mux, Cat, Repl, Array, Signal, ClockSignal, ResetSignal +from .ast import Display from .dsl import Module from .cd import ClockDomain from .ir import Elaboratable, Fragment, Instance @@ -15,6 +16,7 @@ __all__ = [ "Shape", "unsigned", "signed", "Value", "Const", "C", "Mux", "Cat", "Repl", "Array", "Signal", "ClockSignal", "ResetSignal", + "Display", "Module", "ClockDomain", "Elaboratable", "Fragment", "Instance", diff --git a/amaranth/hdl/ast.py b/amaranth/hdl/ast.py index 7655d8a48..858504642 100644 --- a/amaranth/hdl/ast.py +++ b/amaranth/hdl/ast.py @@ -1,5 +1,7 @@ from abc import ABCMeta, abstractmethod import inspect +import re +import string import warnings import functools from collections import OrderedDict @@ -20,8 +22,8 @@ "Signal", "ClockSignal", "ResetSignal", "ValueCastable", "Sample", "Past", "Stable", "Rose", "Fell", "Initial", - "Statement", "Switch", - "Property", "Assign", "Assert", "Assume", "Cover", + "Statement", "Switch", "Assign", + "Display", "Property", "Assert", "Assume", "Cover", "ValueKey", "ValueDict", "ValueSet", "SignalKey", "SignalDict", "SignalSet", ] @@ -1528,6 +1530,79 @@ def __repr__(self): return "(eq {!r} {!r})".format(self.lhs, self.rhs) +class _DisplayFormatter(string.Formatter): + _ESCAPE_TRANS = str.maketrans({"{": "{{", "}": "}}"}) + + @classmethod + def escape(cls, string): + return string.translate(cls._ESCAPE_TRANS) + + _FORMAT_RE = re.compile(r""" + ^ + (?: (?P<fill> [ 0])? (?P<align> [<>=]) )? + (?P<sign> [ +-])? + (?P<prefix> \#)? + (?P<zero> 0)? + (?P<width> \d+)? + (?P<type> [bodx])? + $ + """, re.X) + + @classmethod + def _process_spec(cls, format_spec): + m = re.match(cls._FORMAT_RE, format_spec) + if m is None: + raise SyntaxError("Invalid Display format specifier {!r}".format(format_spec)) + return format_spec + + def __init__(self): + self.args = [] + + def format_field(self, value, format_spec): + if isinstance(value, (Value, ValueCastable)): + index = len(self.args) + self.args.append(Value.cast(value)) + return "{{{}:{}}}".format(index, self._process_spec(format_spec)) + else: + return self.escape(format(value, format_spec)) + + def convert_field(self, value, conversion): + if conversion is None: + return value + raise SyntaxError("Conversion specifiers are not supported in Display") + + def parse(self, format_string): + for literal_text, field_name, format_spec, conversion in super().parse(format_string): + yield self.escape(literal_text), field_name, format_spec, conversion + + +@final +class Display(Statement): + def __init__(self, format_string, *args, end="\n", src_loc_at=0, _en=None, **kwargs): + super().__init__(src_loc_at=src_loc_at) + + formatter = _DisplayFormatter() + self.format = formatter.vformat(format_string, args, kwargs) + formatter.escape(end) + self.args = formatter.args + self._en = _en + if self._en is None: + self._en = Signal(reset_less=True, name="$display$en") + self._en.src_loc = self.src_loc + + def __repr__(self): + if self.args: + return "(display {!r} {})".format(self.format, " ".join(map(repr, self.args))) + else: + return "(display {!r})".format(self.format) + + def _lhs_signals(self): + return SignalSet((self._en,)) + + def _rhs_signals(self): + return union((arg._rhs_signals() for arg in self.args), + start=SignalSet()) + + class UnusedProperty(UnusedMustUse): pass @@ -1547,7 +1622,7 @@ def __init__(self, test, *, _check=None, _en=None, name=None, src_loc_at=0): if self._check is None: self._check = Signal(reset_less=True, name="${}$check".format(self._kind)) self._check.src_loc = self.src_loc - if _en is None: + if self._en is None: self._en = Signal(reset_less=True, name="${}$en".format(self._kind)) self._en.src_loc = self.src_loc diff --git a/amaranth/hdl/dsl.py b/amaranth/hdl/dsl.py index 7edb03bc9..95c1f891a 100644 --- a/amaranth/hdl/dsl.py +++ b/amaranth/hdl/dsl.py @@ -485,9 +485,10 @@ def domain_name(domain): self._pop_ctrl() for stmt in Statement.cast(assigns): - if not compat_mode and not isinstance(stmt, (Assign, Assert, Assume, Cover)): + if not compat_mode and not isinstance(stmt, (Assign, Display, Assert, Assume, Cover)): raise SyntaxError( - "Only assignments and property checks may be appended to d.{}" + "Only assignment, display, and property check statements may be appended " + "to d.{}" .format(domain_name(domain))) stmt._MustUse__used = True diff --git a/amaranth/hdl/xfrm.py b/amaranth/hdl/xfrm.py index 72189cb13..6982fc2ea 100644 --- a/amaranth/hdl/xfrm.py +++ b/amaranth/hdl/xfrm.py @@ -161,6 +161,10 @@ def on_Initial(self, value): class StatementVisitor(metaclass=ABCMeta): + @abstractmethod + def on_Display(self, stmt): + pass # :nocov: + @abstractmethod def on_Assign(self, stmt): pass # :nocov: @@ -194,6 +198,8 @@ def replace_statement_src_loc(self, stmt, new_stmt): def on_statement(self, stmt): if type(stmt) is Assign: new_stmt = self.on_Assign(stmt) + elif type(stmt) is Display: + new_stmt = self.on_Display(stmt) elif type(stmt) is Assert: new_stmt = self.on_Assert(stmt) elif type(stmt) is Assume: @@ -226,6 +232,9 @@ def on_value(self, value): def on_Assign(self, stmt): return Assign(self.on_value(stmt.lhs), self.on_value(stmt.rhs)) + def on_Display(self, stmt): + return Display(stmt.format, *map(self.on_value, stmt.args), end="", _en=stmt._en) + def on_Assert(self, stmt): return Assert(self.on_value(stmt.test), _check=stmt._check, _en=stmt._en, name=stmt.name) @@ -379,6 +388,10 @@ def on_Assign(self, stmt): self.on_value(stmt.lhs) self.on_value(stmt.rhs) + def on_Display(self, stmt): + for arg in stmt.args: + self.on_value(arg) + def on_property(self, stmt): self.on_value(stmt.test) @@ -588,10 +601,11 @@ class SwitchCleaner(StatementVisitor): def on_ignore(self, stmt): return stmt - on_Assign = on_ignore - on_Assert = on_ignore - on_Assume = on_ignore - on_Cover = on_ignore + on_Assign = on_ignore + on_Display = on_ignore + on_Assert = on_ignore + on_Assume = on_ignore + on_Cover = on_ignore def on_Switch(self, stmt): cases = OrderedDict((k, self.on_statement(s)) for k, s in stmt.cases.items()) @@ -639,14 +653,15 @@ def on_Assign(self, stmt): if lhs_signals: self.unify(*stmt._lhs_signals()) - def on_property(self, stmt): + def on_cell(self, stmt): lhs_signals = stmt._lhs_signals() if lhs_signals: self.unify(*stmt._lhs_signals()) - on_Assert = on_property - on_Assume = on_property - on_Cover = on_property + on_Display = on_cell + on_Assert = on_cell + on_Assume = on_cell + on_Cover = on_cell def on_Switch(self, stmt): for case_stmts in stmt.cases.values(): @@ -674,14 +689,15 @@ def on_Assign(self, stmt): if any_lhs_signal in self.signals: return stmt - def on_property(self, stmt): + def on_cell(self, stmt): any_lhs_signal = next(iter(stmt._lhs_signals())) if any_lhs_signal in self.signals: return stmt - on_Assert = on_property - on_Assume = on_property - on_Cover = on_property + on_Display = on_cell + on_Assert = on_cell + on_Assume = on_cell + on_Cover = on_cell class _ControlInserter(FragmentTransformer): diff --git a/amaranth/sim/_pyrtl.py b/amaranth/sim/_pyrtl.py index cd78c6f4a..4441a2c9c 100644 --- a/amaranth/sim/_pyrtl.py +++ b/amaranth/sim/_pyrtl.py @@ -354,18 +354,28 @@ def __init__(self, state, emitter, *, inputs=None, outputs=None): self.rhs = _RHSValueCompiler(state, emitter, mode="curr", inputs=inputs) self.lhs = _LHSValueCompiler(state, emitter, rhs=self.rhs, outputs=outputs) - def on_statements(self, stmts): - for stmt in stmts: - self(stmt) - if not stmts: - self.emitter.append("pass") + def _prepare_rhs(self, value): + value_mask = (1 << len(value)) - 1 + if value.shape().signed: + return f"sign({value_mask:#x} & {self.rhs(value)}, {-1 << (len(value) - 1):#x})" + else: # unsigned + return f"({value_mask:#x} & {self.rhs(value)})" def on_Assign(self, stmt): - gen_rhs_value = self.rhs(stmt.rhs) # check for oversized value before generating mask - gen_rhs = f"({(1 << len(stmt.rhs)) - 1:#x} & {gen_rhs_value})" - if stmt.rhs.shape().signed: - gen_rhs = f"sign({gen_rhs}, {-1 << (len(stmt.rhs) - 1):#x})" - return self.lhs(stmt.lhs)(gen_rhs) + return self.lhs(stmt.lhs)(self._prepare_rhs(stmt.rhs)) + + def on_Display(self, stmt): + gen_args = [self._prepare_rhs(arg) for arg in stmt.args] + self.emitter.append(f"print({stmt.format!r}.format({', '.join(gen_args)}), end='')") + + def on_Assert(self, stmt): + raise NotImplementedError # :nocov: + + def on_Assume(self, stmt): + raise NotImplementedError # :nocov: + + def on_Cover(self, stmt): + raise NotImplementedError # :nocov: def on_Switch(self, stmt): gen_test_value = self.rhs(stmt.test) # check for oversized value before generating mask @@ -390,14 +400,11 @@ def on_Switch(self, stmt): with self.emitter.indent(): self(stmts) - def on_Assert(self, stmt): - raise NotImplementedError # :nocov: - - def on_Assume(self, stmt): - raise NotImplementedError # :nocov: - - def on_Cover(self, stmt): - raise NotImplementedError # :nocov: + def on_statements(self, stmts): + for stmt in stmts: + self(stmt) + if not stmts: + self.emitter.append("pass") @classmethod def compile(cls, state, stmt): diff --git a/tests/test_hdl_dsl.py b/tests/test_hdl_dsl.py index 9fc210ff5..ade0fc577 100644 --- a/tests/test_hdl_dsl.py +++ b/tests/test_hdl_dsl.py @@ -87,7 +87,8 @@ def test_d_wrong(self): def test_d_asgn_wrong(self): m = Module() with self.assertRaisesRegex(SyntaxError, - r"^Only assignments and property checks may be appended to d\.sync$"): + r"^Only assignment, display, and property check statements " + r"may be appended to d\.sync$"): m.d.sync += Switch(self.s1, {}) def test_comb_wrong(self): From 90b6583570597f9c156fd2742923e3624afacb51 Mon Sep 17 00:00:00 2001 From: Charlotte <charlotte@lottia.net> Date: Tue, 22 Aug 2023 12:19:00 +1000 Subject: [PATCH 2/2] back.rtlil: use RTLIL `$print` cell for Display statement. --- amaranth/back/rtlil.py | 16 +++++++++++- amaranth/hdl/ast.py | 58 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/amaranth/back/rtlil.py b/amaranth/back/rtlil.py index e6e968588..2681f4e3a 100644 --- a/amaranth/back/rtlil.py +++ b/amaranth/back/rtlil.py @@ -738,7 +738,21 @@ def on_Assign(self, stmt): self._case.assign(self.lhs_compiler(stmt.lhs), rhs_sigspec) def on_Display(self, stmt): - raise NotImplementedError + self(stmt._en.eq(1)) + en_wire = self.rhs_compiler(stmt._en) + + self.state.rtlil.cell("$print", params={ + "FORMAT": stmt.rtlil_format, + "ARGS_WIDTH": sum(len(arg) for arg in stmt.args), + "TRG_ENABLE": 0, + "TRG_WIDTH": 0, + "TRG_POLARITY": 0, + "PRIORITY": 0, + }, ports={ + "\\TRG": "{}", + "\\EN": en_wire, + "\\ARGS": f"{{ {' '.join(self.rhs_compiler(arg) for arg in stmt.args)} }}", + }, src=_src(stmt.src_loc), name="display") def on_property(self, stmt): self(stmt._check.eq(stmt.test)) diff --git a/amaranth/hdl/ast.py b/amaranth/hdl/ast.py index 858504642..3472c0d6b 100644 --- a/amaranth/hdl/ast.py +++ b/amaranth/hdl/ast.py @@ -1576,6 +1576,60 @@ def parse(self, format_string): yield self.escape(literal_text), field_name, format_spec, conversion +class _DisplayRtlilFormatter(_DisplayFormatter): + def format_field(self, value, format_spec): + if isinstance(value, (Value, ValueCastable)): + m = re.match(self._FORMAT_RE, format_spec) + if m is None: + raise SyntaxError("Invalid Display format specifier {!r}".format(format_spec)) + + # Reference for RTLIL format string syntax: + # https://github.com/YosysHQ/yosys/blob/6405bbab/docs/source/CHAPTER_CellLib.rst#debugging-cells + if m["align"] in ('>', '=', None): + justify = ">" + elif m["align"] == '<': + justify = "<" + else: + raise SyntaxError(f"{m['align']!r} alignment not supported by RTLIL backend") + + if m["fill"] in (' ', None): + padding = ' ' + elif m["fill"] == '0': + padding = '0' + else: + assert False + + if m["width"] is not None: + width = m["width"] + else: + width = '' + + if m["type"] in ('b', 'o', 'd'): + base = m["type"] + elif m["type"] == 'x': + base = 'h' + elif m["type"] is None: + base = 'd' + + if m["sign"] == '+': + lplus = '+' + elif m["sign"] in ('-', None): + lplus = '' + else: + raise SyntaxError(f"{m['sign']!r} sign not supported by RTLIL backend") + + v = Value.cast(value) + + if v.shape().signed: + sign = 's' + else: + sign = 'u' + + return f"{{{len(v)}:{justify}{padding}{width}{base}{lplus}{sign}}}" + else: + return super().format_field(value, format_spec) + + @final class Display(Statement): def __init__(self, format_string, *args, end="\n", src_loc_at=0, _en=None, **kwargs): @@ -1584,6 +1638,10 @@ def __init__(self, format_string, *args, end="\n", src_loc_at=0, _en=None, **kwa formatter = _DisplayFormatter() self.format = formatter.vformat(format_string, args, kwargs) + formatter.escape(end) self.args = formatter.args + + rtlil_formatter = _DisplayRtlilFormatter() + self.rtlil_format = rtlil_formatter.vformat(format_string, args, kwargs) + rtlil_formatter.escape(end) + self._en = _en if self._en is None: self._en = Signal(reset_less=True, name="$display$en")