Implement RFC 50: Print and string formatting.

Co-authored-by: Catherine <whitequark@whitequark.org>
This commit is contained in:
Wanda 2024-03-06 05:23:47 +01:00 committed by Catherine
parent 715a8d4934
commit bfe541a6d7
20 changed files with 1090 additions and 112 deletions

View file

@ -1,3 +1,5 @@
# amaranth: UnusedPrint=no, UnusedProperty
import warnings
from enum import Enum, EnumMeta
@ -329,8 +331,6 @@ class ValueTestCase(FHDLTestCase):
r"^Cannot slice value with a value; use Value.bit_select\(\) or Value.word_select\(\) instead$"):
Const(31)[s:s+3]
def test_shift_left(self):
self.assertRepr(Const(256, unsigned(9)).shift_left(0),
"(cat (const 0'd0) (const 9'd256))")
@ -452,6 +452,12 @@ class ValueTestCase(FHDLTestCase):
s = Const(10).replicate(3)
self.assertEqual(repr(s), "(cat (const 4'd10) (const 4'd10) (const 4'd10))")
def test_format_wrong(self):
sig = Signal()
with self.assertRaisesRegex(TypeError,
r"^Value \(sig sig\) cannot be converted to string."):
f"{sig}"
class ConstTestCase(FHDLTestCase):
def test_shape(self):
@ -1494,6 +1500,151 @@ class InitialTestCase(FHDLTestCase):
self.assertEqual(i.shape(), unsigned(1))
class FormatTestCase(FHDLTestCase):
def test_construct(self):
a = Signal()
b = Signal()
c = Signal()
self.assertRepr(Format("abc"), "(format 'abc')")
fmt = Format("{{abc}}")
self.assertRepr(fmt, "(format '{{abc}}')")
self.assertEqual(fmt._chunks, ("{abc}",))
fmt = Format("{abc}", abc="{def}")
self.assertRepr(fmt, "(format '{{def}}')")
self.assertEqual(fmt._chunks, ("{def}",))
fmt = Format("a: {a:0{b}}, b: {b}", a=13, b=4)
self.assertRepr(fmt, "(format 'a: 0013, b: 4')")
fmt = Format("a: {a:0{b}x}, b: {b}", a=a, b=4)
self.assertRepr(fmt, "(format 'a: {:04x}, b: 4' (sig a))")
fmt = Format("a: {a}, b: {b}, a: {a}", a=a, b=b)
self.assertRepr(fmt, "(format 'a: {}, b: {}, a: {}' (sig a) (sig b) (sig a))")
fmt = Format("a: {0}, b: {1}, a: {0}", a, b)
self.assertRepr(fmt, "(format 'a: {}, b: {}, a: {}' (sig a) (sig b) (sig a))")
fmt = Format("a: {}, b: {}", a, b)
self.assertRepr(fmt, "(format 'a: {}, b: {}' (sig a) (sig b))")
subfmt = Format("a: {:2x}, b: {:3x}", a, b)
fmt = Format("sub: {}, c: {:4x}", subfmt, c)
self.assertRepr(fmt, "(format 'sub: a: {:2x}, b: {:3x}, c: {:4x}' (sig a) (sig b) (sig c))")
def test_construct_wrong(self):
a = Signal()
b = Signal(signed(16))
with self.assertRaisesRegex(ValueError,
r"^cannot switch from manual field specification to automatic field numbering$"):
Format("{0}, {}", a, b)
with self.assertRaisesRegex(ValueError,
r"^cannot switch from automatic field numbering to manual field specification$"):
Format("{}, {1}", a, b)
with self.assertRaisesRegex(TypeError,
r"^'ValueCastable' formatting is not supported$"):
Format("{}", MockValueCastable(Const(0)))
with self.assertRaisesRegex(ValueError,
r"^Format specifiers \('s'\) cannot be used for 'Format' objects$"):
Format("{:s}", Format(""))
with self.assertRaisesRegex(ValueError,
r"^format positional argument 1 was not used$"):
Format("{}", a, b)
with self.assertRaisesRegex(ValueError,
r"^format keyword argument 'b' was not used$"):
Format("{a}", a=a, b=b)
with self.assertRaisesRegex(ValueError,
r"^Invalid format specifier 'meow'$"):
Format("{a:meow}", a=a)
with self.assertRaisesRegex(ValueError,
r"^Alignment '\^' is not supported$"):
Format("{a:^13}", a=a)
with self.assertRaisesRegex(ValueError,
r"^Grouping option ',' is not supported$"):
Format("{a:,}", a=a)
with self.assertRaisesRegex(ValueError,
r"^Presentation type 'n' is not supported$"):
Format("{a:n}", a=a)
with self.assertRaisesRegex(ValueError,
r"^Cannot print signed value with format specifier 'c'$"):
Format("{b:c}", b=b)
with self.assertRaisesRegex(ValueError,
r"^Value width must be divisible by 8 with format specifier 's'$"):
Format("{a:s}", a=a)
with self.assertRaisesRegex(ValueError,
r"^Alignment '=' is not allowed with format specifier 'c'$"):
Format("{a:=13c}", a=a)
with self.assertRaisesRegex(ValueError,
r"^Sign is not allowed with format specifier 'c'$"):
Format("{a:+13c}", a=a)
with self.assertRaisesRegex(ValueError,
r"^Zero fill is not allowed with format specifier 'c'$"):
Format("{a:013c}", a=a)
with self.assertRaisesRegex(ValueError,
r"^Alternate form is not allowed with format specifier 'c'$"):
Format("{a:#13c}", a=a)
with self.assertRaisesRegex(ValueError,
r"^Cannot specify '_' with format specifier 'c'$"):
Format("{a:_c}", a=a)
def test_plus(self):
a = Signal()
b = Signal()
fmt_a = Format("a = {};", a)
fmt_b = Format("b = {};", b)
fmt = fmt_a + fmt_b
self.assertRepr(fmt, "(format 'a = {};b = {};' (sig a) (sig b))")
self.assertEqual(fmt._chunks[2], ";b = ")
def test_plus_wrong(self):
with self.assertRaisesRegex(TypeError,
r"^unsupported operand type\(s\) for \+: 'Format' and 'str'$"):
Format("") + ""
def test_format_wrong(self):
fmt = Format("")
with self.assertRaisesRegex(TypeError,
r"^Format object .* cannot be converted to string."):
f"{fmt}"
class PrintTestCase(FHDLTestCase):
def test_construct(self):
a = Signal()
b = Signal()
p = Print("abc")
self.assertRepr(p, "(print (format 'abc\\n'))")
p = Print("abc", "def")
self.assertRepr(p, "(print (format 'abc def\\n'))")
p = Print("abc", b)
self.assertRepr(p, "(print (format 'abc {}\\n' (sig b)))")
p = Print(a, b, end="", sep=", ")
self.assertRepr(p, "(print (format '{}, {}' (sig a) (sig b)))")
p = Print(Format("a: {a:04x}", a=a))
self.assertRepr(p, "(print (format 'a: {:04x}\\n' (sig a)))")
def test_construct_wrong(self):
with self.assertRaisesRegex(TypeError,
r"^'sep' must be a string, not 13$"):
Print("", sep=13)
with self.assertRaisesRegex(TypeError,
r"^'end' must be a string, not 13$"):
Print("", end=13)
class AssertTestCase(FHDLTestCase):
def test_construct(self):
a = Signal()
b = Signal()
p = Assert(a)
self.assertRepr(p, "(assert (sig a))")
p = Assert(a, "abc")
self.assertRepr(p, "(assert (sig a) (format 'abc'))")
p = Assert(a, Format("a = {}, b = {}", a, b))
self.assertRepr(p, "(assert (sig a) (format 'a = {}, b = {}' (sig a) (sig b)))")
def test_construct_wrong(self):
a = Signal()
b = Signal()
with self.assertRaisesRegex(TypeError,
r"^Property message must be None, str, or Format, not \(sig b\)$"):
Assert(a, b)
class SwitchTestCase(FHDLTestCase):
def test_default_case(self):
s = Switch(Const(0), {None: []})

View file

@ -87,7 +87,7 @@ class DSLTestCase(FHDLTestCase):
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 assignments, prints, and property checks may be appended to d\.sync$"):
m.d.sync += Switch(self.s1, {})
def test_comb_wrong(self):

View file

@ -214,12 +214,12 @@ class FragmentPortsTestCase(FHDLTestCase):
(cell 1 3 (~ 2.0))
(cell 2 4 (~ 6.0))
(cell 3 4 (assignment_list 1'd0 (1 0:1 1'd1)))
(cell 4 4 (assert None 0.2 3.0))
(cell 4 4 (assert 0.2 3.0 None))
(cell 5 5 (~ 6.0))
(cell 6 7 (~ 10.0))
(cell 7 7 (~ 0.2))
(cell 8 7 (assignment_list 1'd0 (1 0:1 1'd1)))
(cell 9 7 (assert None 7.0 8.0))
(cell 9 7 (assert 7.0 8.0 None))
(cell 10 8 (~ 0.2))
)
""")
@ -3146,6 +3146,69 @@ class SwitchTestCase(FHDLTestCase):
)
""")
def test_print(self):
m = Module()
a = Signal(6)
b = Signal(signed(8))
en = Signal()
m.domains.a = ClockDomain()
m.domains.b = ClockDomain(async_reset=True)
m.domains.c = ClockDomain(reset_less=True, clk_edge="neg")
with m.If(en):
m.d.comb += Print(a, end="")
m.d.comb += Print(b)
m.d.a += Print(a, b)
m.d.b += Print(Format("values: {:02x}, {:+d}", a, b))
m.d.c += Print("meow")
nl = build_netlist(Fragment.get(m, None), [
a, b, en,
ClockSignal("a"), ResetSignal("a"),
ClockSignal("b"), ResetSignal("b"),
ClockSignal("c"),
])
self.assertRepr(nl, """
(
(module 0 None ('top')
(input 'a' 0.2:8)
(input 'b' 0.8:16)
(input 'en' 0.16)
(input 'a_clk' 0.17)
(input 'a_rst' 0.18)
(input 'b_clk' 0.19)
(input 'b_rst' 0.20)
(input 'c_clk' 0.21)
)
(cell 0 0 (top
(input 'a' 2:8)
(input 'b' 8:16)
(input 'en' 16:17)
(input 'a_clk' 17:18)
(input 'a_rst' 18:19)
(input 'b_clk' 19:20)
(input 'b_rst' 20:21)
(input 'c_clk' 21:22)
))
(cell 1 0 (matches 0.16 1))
(cell 2 0 (priority_match 1 1.0))
(cell 3 0 (assignment_list 1'd0 (2.0 0:1 1'd1)))
(cell 4 0 (print 3.0 ((u 0.2:8 ''))))
(cell 5 0 (assignment_list 1'd0 (2.0 0:1 1'd1)))
(cell 6 0 (print 5.0 ((s 0.8:16 '') '\\n')))
(cell 7 0 (matches 0.16 1))
(cell 8 0 (priority_match 1 7.0))
(cell 9 0 (assignment_list 1'd0 (8.0 0:1 1'd1)))
(cell 10 0 (print 9.0 pos 0.17 ((u 0.2:8 '') ' ' (s 0.8:16 '') '\\n')))
(cell 11 0 (matches 0.16 1))
(cell 12 0 (priority_match 1 11.0))
(cell 13 0 (assignment_list 1'd0 (12.0 0:1 1'd1)))
(cell 14 0 (print 13.0 pos 0.19 ('values: ' (u 0.2:8 '02x') ', ' (s 0.8:16 '+d') '\\n')))
(cell 15 0 (matches 0.16 1))
(cell 16 0 (priority_match 1 15.0))
(cell 17 0 (assignment_list 1'd0 (16.0 0:1 1'd1)))
(cell 18 0 (print 17.0 neg 0.21 ('meow\\n')))
)
""")
def test_assert(self):
m = Module()
i = Signal(6)
@ -3154,11 +3217,11 @@ class SwitchTestCase(FHDLTestCase):
m.domains.c = ClockDomain(reset_less=True, clk_edge="neg")
with m.If(i[5]):
m.d.comb += Assert(i[0])
m.d.comb += Assume(i[1], name="a")
m.d.comb += Assume(i[1], "aaa")
m.d.a += Assert(i[2])
m.d.b += Assume(i[3], name="b")
m.d.c += Cover(i[4], name="c")
m.d.comb += Cover(i, name="d")
m.d.b += Assume(i[3], Format("value: {}", i))
m.d.c += Cover(i[4], "c")
m.d.comb += Cover(i, "d")
nl = build_netlist(Fragment.get(m, None), [
i,
ClockSignal("a"), ResetSignal("a"),
@ -3186,25 +3249,23 @@ class SwitchTestCase(FHDLTestCase):
(cell 1 0 (matches 0.7 1))
(cell 2 0 (priority_match 1 1.0))
(cell 3 0 (assignment_list 1'd0 (2.0 0:1 1'd1)))
(cell 4 0 (assert None 0.2 3.0))
(cell 4 0 (assert 0.2 3.0 None))
(cell 5 0 (assignment_list 1'd0 (2.0 0:1 1'd1)))
(cell 6 0 (assume 'a' 0.3 5.0))
(cell 6 0 (assume 0.3 5.0 ('aaa')))
(cell 7 0 (b 0.2:8))
(cell 8 0 (assignment_list 1'd0 (2.0 0:1 1'd1)))
(cell 9 0 (cover 'd' 7.0 8.0))
(cell 9 0 (cover 7.0 8.0 ('d')))
(cell 10 0 (matches 0.7 1))
(cell 11 0 (priority_match 1 10.0))
(cell 12 0 (assignment_list 1'd0 (11.0 0:1 1'd1)))
(cell 13 0 (assert None 0.4 12.0 pos 0.8))
(cell 13 0 (assert 0.4 12.0 pos 0.8 None))
(cell 14 0 (matches 0.7 1))
(cell 15 0 (priority_match 1 14.0))
(cell 16 0 (assignment_list 1'd0 (15.0 0:1 1'd1)))
(cell 17 0 (assume 'b' 0.5 16.0 pos 0.10))
(cell 17 0 (assume 0.5 16.0 pos 0.10 ('value: ' (u 0.2:8 ''))))
(cell 18 0 (matches 0.7 1))
(cell 19 0 (priority_match 1 18.0))
(cell 20 0 (assignment_list 1'd0 (19.0 0:1 1'd1)))
(cell 21 0 (cover 'c' 0.6 20.0 neg 0.12))
(cell 21 0 (cover 0.6 20.0 neg 0.12 ('c')))
)
""")

View file

@ -1,5 +1,4 @@
from amaranth.hdl import *
from amaranth.asserts import *
from amaranth.sim import *
from amaranth.lib.coding import *

View file

@ -3,7 +3,7 @@
import warnings
from amaranth.hdl import *
from amaranth.asserts import *
from amaranth.asserts import Initial, AnyConst
from amaranth.sim import *
from amaranth.lib.fifo import *
from amaranth.lib.memory import *

View file

@ -1,6 +1,8 @@
import os
import warnings
from contextlib import contextmanager
from contextlib import contextmanager, redirect_stdout
from io import StringIO
from textwrap import dedent
from amaranth._utils import flatten
from amaranth.hdl._ast import *
@ -416,7 +418,7 @@ class SimulatorUnitTestCase(FHDLTestCase):
class SimulatorIntegrationTestCase(FHDLTestCase):
@contextmanager
def assertSimulation(self, module, deadline=None):
def assertSimulation(self, module, *, deadline=None):
sim = Simulator(module)
yield sim
with sim.write_vcd("test.vcd", "test.gtkw"):
@ -1074,6 +1076,104 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
self.assertEqual((yield o), 1)
sim.add_testbench(process)
def test_print(self):
m = Module()
ctr = Signal(16)
m.d.sync += ctr.eq(ctr + 1)
with m.If(ctr % 3 == 0):
m.d.sync += Print(Format("Counter: {ctr:03d}", ctr=ctr))
output = StringIO()
with redirect_stdout(output):
with self.assertSimulation(m) as sim:
sim.add_clock(1e-6, domain="sync")
def process():
yield Delay(1e-5)
sim.add_testbench(process)
self.assertEqual(output.getvalue(), dedent("""\
Counter: 000
Counter: 003
Counter: 006
Counter: 009
"""))
def test_print(self):
def enc(s):
return Cat(
Const(b, 8)
for b in s.encode()
)
m = Module()
ctr = Signal(16)
m.d.sync += ctr.eq(ctr + 1)
msg = Signal(8 * 8)
with m.If(ctr == 0):
m.d.comb += msg.eq(enc("zero"))
with m.Else():
m.d.comb += msg.eq(enc("non-zero"))
with m.If(ctr % 3 == 0):
m.d.sync += Print(Format("Counter: {:>8s}", msg))
output = StringIO()
with redirect_stdout(output):
with self.assertSimulation(m) as sim:
sim.add_clock(1e-6, domain="sync")
def process():
yield Delay(1e-5)
sim.add_testbench(process)
self.assertEqual(output.getvalue(), dedent("""\
Counter: zero
Counter: non-zero
Counter: non-zero
Counter: non-zero
"""))
def test_assert(self):
m = Module()
ctr = Signal(16)
m.d.sync += ctr.eq(ctr + 1)
m.d.sync += Assert(ctr < 4, Format("Counter too large: {}", ctr))
with self.assertRaisesRegex(AssertionError,
r"^Assertion violated: Counter too large: 4$"):
with self.assertSimulation(m) as sim:
sim.add_clock(1e-6, domain="sync")
def process():
yield Delay(1e-5)
sim.add_testbench(process)
def test_assume(self):
m = Module()
ctr = Signal(16)
m.d.sync += ctr.eq(ctr + 1)
m.d.comb += Assume(ctr < 4)
with self.assertRaisesRegex(AssertionError,
r"^Assumption violated$"):
with self.assertSimulation(m) as sim:
sim.add_clock(1e-6, domain="sync")
def process():
yield Delay(1e-5)
sim.add_testbench(process)
def test_cover(self):
m = Module()
ctr = Signal(16)
m.d.sync += ctr.eq(ctr + 1)
cover = Cover(ctr % 3 == 0, Format("Counter: {ctr:03d}", ctr=ctr))
m.d.sync += cover
m.d.sync += Cover(ctr % 3 == 1)
output = StringIO()
with redirect_stdout(output):
with self.assertSimulation(m) as sim:
sim.add_clock(1e-6, domain="sync")
def process():
yield Delay(1e-5)
sim.add_testbench(process)
self.assertRegex(output.getvalue(), dedent(r"""
Coverage hit at .*test_sim\.py:\d+: Counter: 000
Coverage hit at .*test_sim\.py:\d+: Counter: 003
Coverage hit at .*test_sim\.py:\d+: Counter: 006
Coverage hit at .*test_sim\.py:\d+: Counter: 009
""").lstrip())
class SimulatorRegressionTestCase(FHDLTestCase):
def test_bug_325(self):