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")