amaranth/nmigen/test/test_hdl_dsl.py
whitequark 6fd7cbad0d hdl.dsl: don't allow inheriting from Module.
`Module` is an object with a lot of complex and sometimes fragile
behavior that overrides Python attribute accessors and so on.
To prevent user designs from breaking when it is changed, it is not
supposed to be inherited from (unlike in Migen), but rather returned
from the elaborate() method. This commit makes sure it will not be
inherited from by accident (most likely by users familiar with
Migen).

Fixes #286.
2020-02-01 02:15:45 +00:00

737 lines
22 KiB
Python

# nmigen: UnusedElaboratable=no
from collections import OrderedDict
from enum import Enum
from ..hdl.ast import *
from ..hdl.cd import *
from ..hdl.dsl import *
from .utils import *
class DSLTestCase(FHDLTestCase):
def setUp(self):
self.s1 = Signal()
self.s2 = Signal()
self.s3 = Signal()
self.c1 = Signal()
self.c2 = Signal()
self.c3 = Signal()
self.w1 = Signal(4)
def test_cant_inherit(self):
with self.assertRaises(SyntaxError,
msg="Instead of inheriting from `Module`, inherit from `Elaboratable` and "
"return a `Module` from the `elaborate(self, platform)` method"):
class ORGate(Module):
pass
def test_d_comb(self):
m = Module()
m.d.comb += self.c1.eq(1)
m._flush()
self.assertEqual(m._driving[self.c1], None)
self.assertRepr(m._statements, """(
(eq (sig c1) (const 1'd1))
)""")
def test_d_sync(self):
m = Module()
m.d.sync += self.c1.eq(1)
m._flush()
self.assertEqual(m._driving[self.c1], "sync")
self.assertRepr(m._statements, """(
(eq (sig c1) (const 1'd1))
)""")
def test_d_pix(self):
m = Module()
m.d.pix += self.c1.eq(1)
m._flush()
self.assertEqual(m._driving[self.c1], "pix")
self.assertRepr(m._statements, """(
(eq (sig c1) (const 1'd1))
)""")
def test_d_index(self):
m = Module()
m.d["pix"] += self.c1.eq(1)
m._flush()
self.assertEqual(m._driving[self.c1], "pix")
self.assertRepr(m._statements, """(
(eq (sig c1) (const 1'd1))
)""")
def test_d_no_conflict(self):
m = Module()
m.d.comb += self.w1[0].eq(1)
m.d.comb += self.w1[1].eq(1)
def test_d_conflict(self):
m = Module()
with self.assertRaises(SyntaxError,
msg="Driver-driver conflict: trying to drive (sig c1) from d.sync, but it "
"is already driven from d.comb"):
m.d.comb += self.c1.eq(1)
m.d.sync += self.c1.eq(1)
def test_d_wrong(self):
m = Module()
with self.assertRaises(AttributeError,
msg="Cannot assign 'd.pix' attribute; did you mean 'd.pix +='?"):
m.d.pix = None
def test_d_asgn_wrong(self):
m = Module()
with self.assertRaises(SyntaxError,
msg="Only assignments and property checks may be appended to d.sync"):
m.d.sync += Switch(self.s1, {})
def test_comb_wrong(self):
m = Module()
with self.assertRaises(AttributeError,
msg="'Module' object has no attribute 'comb'; did you mean 'd.comb'?"):
m.comb += self.c1.eq(1)
def test_sync_wrong(self):
m = Module()
with self.assertRaises(AttributeError,
msg="'Module' object has no attribute 'sync'; did you mean 'd.sync'?"):
m.sync += self.c1.eq(1)
def test_attr_wrong(self):
m = Module()
with self.assertRaises(AttributeError,
msg="'Module' object has no attribute 'nonexistentattr'"):
m.nonexistentattr
def test_d_suspicious(self):
m = Module()
with self.assertWarns(SyntaxWarning,
msg="Using '<module>.d.submodules' would add statements to clock domain "
"'submodules'; did you mean <module>.submodules instead?"):
m.d.submodules += []
def test_clock_signal(self):
m = Module()
m.d.comb += ClockSignal("pix").eq(ClockSignal())
self.assertRepr(m._statements, """
(
(eq (clk pix) (clk sync))
)
""")
def test_reset_signal(self):
m = Module()
m.d.comb += ResetSignal("pix").eq(1)
self.assertRepr(m._statements, """
(
(eq (rst pix) (const 1'd1))
)
""")
def test_sample_domain(self):
m = Module()
i = Signal()
o1 = Signal()
o2 = Signal()
o3 = Signal()
m.d.sync += o1.eq(Past(i))
m.d.pix += o2.eq(Past(i))
m.d.pix += o3.eq(Past(i, domain="sync"))
f = m.elaborate(platform=None)
self.assertRepr(f.statements, """
(
(eq (sig o1) (sample (sig i) @ sync[1]))
(eq (sig o2) (sample (sig i) @ pix[1]))
(eq (sig o3) (sample (sig i) @ sync[1]))
)
""")
def test_If(self):
m = Module()
with m.If(self.s1):
m.d.comb += self.c1.eq(1)
m._flush()
self.assertRepr(m._statements, """
(
(switch (cat (sig s1))
(case 1 (eq (sig c1) (const 1'd1)))
)
)
""")
def test_If_Elif(self):
m = Module()
with m.If(self.s1):
m.d.comb += self.c1.eq(1)
with m.Elif(self.s2):
m.d.sync += self.c2.eq(0)
m._flush()
self.assertRepr(m._statements, """
(
(switch (cat (sig s1) (sig s2))
(case -1 (eq (sig c1) (const 1'd1)))
(case 1- (eq (sig c2) (const 1'd0)))
)
)
""")
def test_If_Elif_Else(self):
m = Module()
with m.If(self.s1):
m.d.comb += self.c1.eq(1)
with m.Elif(self.s2):
m.d.sync += self.c2.eq(0)
with m.Else():
m.d.comb += self.c3.eq(1)
m._flush()
self.assertRepr(m._statements, """
(
(switch (cat (sig s1) (sig s2))
(case -1 (eq (sig c1) (const 1'd1)))
(case 1- (eq (sig c2) (const 1'd0)))
(default (eq (sig c3) (const 1'd1)))
)
)
""")
def test_If_If(self):
m = Module()
with m.If(self.s1):
m.d.comb += self.c1.eq(1)
with m.If(self.s2):
m.d.comb += self.c2.eq(1)
m._flush()
self.assertRepr(m._statements, """
(
(switch (cat (sig s1))
(case 1 (eq (sig c1) (const 1'd1)))
)
(switch (cat (sig s2))
(case 1 (eq (sig c2) (const 1'd1)))
)
)
""")
def test_If_nested_If(self):
m = Module()
with m.If(self.s1):
m.d.comb += self.c1.eq(1)
with m.If(self.s2):
m.d.comb += self.c2.eq(1)
m._flush()
self.assertRepr(m._statements, """
(
(switch (cat (sig s1))
(case 1 (eq (sig c1) (const 1'd1))
(switch (cat (sig s2))
(case 1 (eq (sig c2) (const 1'd1)))
)
)
)
)
""")
def test_If_dangling_Else(self):
m = Module()
with m.If(self.s1):
m.d.comb += self.c1.eq(1)
with m.If(self.s2):
m.d.comb += self.c2.eq(1)
with m.Else():
m.d.comb += self.c3.eq(1)
m._flush()
self.assertRepr(m._statements, """
(
(switch (cat (sig s1))
(case 1
(eq (sig c1) (const 1'd1))
(switch (cat (sig s2))
(case 1 (eq (sig c2) (const 1'd1)))
)
)
(default
(eq (sig c3) (const 1'd1))
)
)
)
""")
def test_Elif_wrong(self):
m = Module()
with self.assertRaises(SyntaxError,
msg="Elif without preceding If"):
with m.Elif(self.s2):
pass
def test_Else_wrong(self):
m = Module()
with self.assertRaises(SyntaxError,
msg="Else without preceding If/Elif"):
with m.Else():
pass
def test_If_wide(self):
m = Module()
with m.If(self.w1):
m.d.comb += self.c1.eq(1)
m._flush()
self.assertRepr(m._statements, """
(
(switch (cat (b (sig w1)))
(case 1 (eq (sig c1) (const 1'd1)))
)
)
""")
def test_If_signed_suspicious(self):
m = Module()
with self.assertWarns(SyntaxWarning,
msg="Signed values in If/Elif conditions usually result from inverting Python "
"booleans with ~, which leads to unexpected results: ~True is -2, which is "
"truthful. Replace `~flag` with `not flag`. (If this is a false positive, "
"silence this warning with `m.If(x)` → `m.If(x.bool())`.)"):
with m.If(~True):
pass
def test_Elif_signed_suspicious(self):
m = Module()
with m.If(0):
pass
with self.assertWarns(SyntaxWarning,
msg="Signed values in If/Elif conditions usually result from inverting Python "
"booleans with ~, which leads to unexpected results: ~True is -2, which is "
"truthful. Replace `~flag` with `not flag`. (If this is a false positive, "
"silence this warning with `m.If(x)` → `m.If(x.bool())`.)"):
with m.Elif(~True):
pass
def test_if_If_Elif_Else(self):
m = Module()
with self.assertRaises(SyntaxError,
msg="`if m.If(...):` does not work; use `with m.If(...)`"):
if m.If(0):
pass
with m.If(0):
pass
with self.assertRaises(SyntaxError,
msg="`if m.Elif(...):` does not work; use `with m.Elif(...)`"):
if m.Elif(0):
pass
with self.assertRaises(SyntaxError,
msg="`if m.Else(...):` does not work; use `with m.Else(...)`"):
if m.Else():
pass
def test_Switch(self):
m = Module()
with m.Switch(self.w1):
with m.Case(3):
m.d.comb += self.c1.eq(1)
with m.Case("11--"):
m.d.comb += self.c2.eq(1)
m._flush()
self.assertRepr(m._statements, """
(
(switch (sig w1)
(case 0011 (eq (sig c1) (const 1'd1)))
(case 11-- (eq (sig c2) (const 1'd1)))
)
)
""")
def test_Switch_default_Case(self):
m = Module()
with m.Switch(self.w1):
with m.Case(3):
m.d.comb += self.c1.eq(1)
with m.Case():
m.d.comb += self.c2.eq(1)
m._flush()
self.assertRepr(m._statements, """
(
(switch (sig w1)
(case 0011 (eq (sig c1) (const 1'd1)))
(default (eq (sig c2) (const 1'd1)))
)
)
""")
def test_Switch_default_Default(self):
m = Module()
with m.Switch(self.w1):
with m.Case(3):
m.d.comb += self.c1.eq(1)
with m.Default():
m.d.comb += self.c2.eq(1)
m._flush()
self.assertRepr(m._statements, """
(
(switch (sig w1)
(case 0011 (eq (sig c1) (const 1'd1)))
(default (eq (sig c2) (const 1'd1)))
)
)
""")
def test_Switch_const_test(self):
m = Module()
with m.Switch(1):
with m.Case(1):
m.d.comb += self.c1.eq(1)
m._flush()
self.assertRepr(m._statements, """
(
(switch (const 1'd1)
(case 1 (eq (sig c1) (const 1'd1)))
)
)
""")
def test_Switch_enum(self):
class Color(Enum):
RED = 1
BLUE = 2
m = Module()
se = Signal(Color)
with m.Switch(se):
with m.Case(Color.RED):
m.d.comb += self.c1.eq(1)
self.assertRepr(m._statements, """
(
(switch (sig se)
(case 01 (eq (sig c1) (const 1'd1)))
)
)
""")
def test_Case_width_wrong(self):
class Color(Enum):
RED = 0b10101010
m = Module()
with m.Switch(self.w1):
with self.assertRaises(SyntaxError,
msg="Case pattern '--' must have the same width as switch value (which is 4)"):
with m.Case("--"):
pass
with self.assertWarns(SyntaxWarning,
msg="Case pattern '10110' is wider than switch value (which has width 4); "
"comparison will never be true"):
with m.Case(0b10110):
pass
with self.assertWarns(SyntaxWarning,
msg="Case pattern '10101010' (Color.RED) is wider than switch value "
"(which has width 4); comparison will never be true"):
with m.Case(Color.RED):
pass
self.assertRepr(m._statements, """
(
(switch (sig w1) )
)
""")
def test_Case_bits_wrong(self):
m = Module()
with m.Switch(self.w1):
with self.assertRaises(SyntaxError,
msg="Case pattern 'abc' must consist of 0, 1, and - (don't care) bits"):
with m.Case("abc"):
pass
def test_Case_pattern_wrong(self):
m = Module()
with m.Switch(self.w1):
with self.assertRaises(SyntaxError,
msg="Case pattern must be an integer, a string, or an enumeration, not 1.0"):
with m.Case(1.0):
pass
def test_Case_outside_Switch_wrong(self):
m = Module()
with self.assertRaises(SyntaxError,
msg="Case is not permitted outside of Switch"):
with m.Case():
pass
def test_If_inside_Switch_wrong(self):
m = Module()
with m.Switch(self.s1):
with self.assertRaises(SyntaxError,
msg="If is not permitted directly inside of Switch; "
"it is permitted inside of Switch Case"):
with m.If(self.s2):
pass
def test_FSM_basic(self):
a = Signal()
b = Signal()
c = Signal()
m = Module()
with m.FSM():
with m.State("FIRST"):
m.d.comb += a.eq(1)
m.next = "SECOND"
with m.State("SECOND"):
m.d.sync += b.eq(~b)
with m.If(c):
m.next = "FIRST"
m._flush()
self.assertRepr(m._statements, """
(
(switch (sig fsm_state)
(case 0
(eq (sig a) (const 1'd1))
(eq (sig fsm_state) (const 1'd1))
)
(case 1
(eq (sig b) (~ (sig b)))
(switch (cat (sig c))
(case 1
(eq (sig fsm_state) (const 1'd0)))
)
)
)
)
""")
self.assertEqual({repr(k): v for k, v in m._driving.items()}, {
"(sig a)": None,
"(sig fsm_state)": "sync",
"(sig b)": "sync",
})
frag = m.elaborate(platform=None)
fsm = frag.find_generated("fsm")
self.assertIsInstance(fsm.state, Signal)
self.assertEqual(fsm.encoding, OrderedDict({
"FIRST": 0,
"SECOND": 1,
}))
self.assertEqual(fsm.decoding, OrderedDict({
0: "FIRST",
1: "SECOND"
}))
def test_FSM_reset(self):
a = Signal()
m = Module()
with m.FSM(reset="SECOND"):
with m.State("FIRST"):
m.d.comb += a.eq(0)
m.next = "SECOND"
with m.State("SECOND"):
m.next = "FIRST"
m._flush()
self.assertRepr(m._statements, """
(
(switch (sig fsm_state)
(case 0
(eq (sig a) (const 1'd0))
(eq (sig fsm_state) (const 1'd1))
)
(case 1
(eq (sig fsm_state) (const 1'd0))
)
)
)
""")
def test_FSM_ongoing(self):
a = Signal()
b = Signal()
m = Module()
with m.FSM() as fsm:
m.d.comb += b.eq(fsm.ongoing("SECOND"))
with m.State("FIRST"):
pass
m.d.comb += a.eq(fsm.ongoing("FIRST"))
with m.State("SECOND"):
pass
m._flush()
self.assertEqual(m._generated["fsm"].state.reset, 1)
self.maxDiff = 10000
self.assertRepr(m._statements, """
(
(eq (sig b) (== (sig fsm_state) (const 1'd0)))
(eq (sig a) (== (sig fsm_state) (const 1'd1)))
(switch (sig fsm_state)
(case 1
)
(case 0
)
)
)
""")
def test_FSM_empty(self):
m = Module()
with m.FSM():
pass
self.assertRepr(m._statements, """
()
""")
def test_FSM_wrong_domain(self):
m = Module()
with self.assertRaises(ValueError,
msg="FSM may not be driven by the 'comb' domain"):
with m.FSM(domain="comb"):
pass
def test_FSM_wrong_redefined(self):
m = Module()
with m.FSM():
with m.State("FOO"):
pass
with self.assertRaises(SyntaxError,
msg="FSM state 'FOO' is already defined"):
with m.State("FOO"):
pass
def test_FSM_wrong_next(self):
m = Module()
with self.assertRaises(SyntaxError,
msg="Only assignment to `m.next` is permitted"):
m.next
with self.assertRaises(SyntaxError,
msg="`m.next = <...>` is only permitted inside an FSM state"):
m.next = "FOO"
with self.assertRaises(SyntaxError,
msg="`m.next = <...>` is only permitted inside an FSM state"):
with m.FSM():
m.next = "FOO"
def test_If_inside_FSM_wrong(self):
m = Module()
with m.FSM():
with m.State("FOO"):
pass
with self.assertRaises(SyntaxError,
msg="If is not permitted directly inside of FSM; "
"it is permitted inside of FSM State"):
with m.If(self.s2):
pass
def test_auto_pop_ctrl(self):
m = Module()
with m.If(self.w1):
m.d.comb += self.c1.eq(1)
m.d.comb += self.c2.eq(1)
self.assertRepr(m._statements, """
(
(switch (cat (b (sig w1)))
(case 1 (eq (sig c1) (const 1'd1)))
)
(eq (sig c2) (const 1'd1))
)
""")
def test_submodule_anon(self):
m1 = Module()
m2 = Module()
m1.submodules += m2
self.assertEqual(m1._anon_submodules, [m2])
self.assertEqual(m1._named_submodules, {})
def test_submodule_anon_multi(self):
m1 = Module()
m2 = Module()
m3 = Module()
m1.submodules += m2, m3
self.assertEqual(m1._anon_submodules, [m2, m3])
self.assertEqual(m1._named_submodules, {})
def test_submodule_named(self):
m1 = Module()
m2 = Module()
m1.submodules.foo = m2
self.assertEqual(m1._anon_submodules, [])
self.assertEqual(m1._named_submodules, {"foo": m2})
def test_submodule_named_index(self):
m1 = Module()
m2 = Module()
m1.submodules["foo"] = m2
self.assertEqual(m1._anon_submodules, [])
self.assertEqual(m1._named_submodules, {"foo": m2})
def test_submodule_wrong(self):
m = Module()
with self.assertRaises(TypeError,
msg="Trying to add 1, which does not implement .elaborate(), as a submodule"):
m.submodules.foo = 1
with self.assertRaises(TypeError,
msg="Trying to add 1, which does not implement .elaborate(), as a submodule"):
m.submodules += 1
def test_submodule_named_conflict(self):
m1 = Module()
m2 = Module()
m1.submodules.foo = m2
with self.assertRaises(NameError, msg="Submodule named 'foo' already exists"):
m1.submodules.foo = m2
def test_submodule_get(self):
m1 = Module()
m2 = Module()
m1.submodules.foo = m2
m3 = m1.submodules.foo
self.assertEqual(m2, m3)
def test_submodule_get_index(self):
m1 = Module()
m2 = Module()
m1.submodules["foo"] = m2
m3 = m1.submodules["foo"]
self.assertEqual(m2, m3)
def test_submodule_get_unset(self):
m1 = Module()
with self.assertRaises(AttributeError, msg="No submodule named 'foo' exists"):
m2 = m1.submodules.foo
with self.assertRaises(AttributeError, msg="No submodule named 'foo' exists"):
m2 = m1.submodules["foo"]
def test_domain_named_implicit(self):
m = Module()
m.domains += ClockDomain("sync")
self.assertEqual(len(m._domains), 1)
def test_domain_named_explicit(self):
m = Module()
m.domains.foo = ClockDomain()
self.assertEqual(len(m._domains), 1)
self.assertEqual(m._domains[0].name, "foo")
def test_lower(self):
m1 = Module()
m1.d.comb += self.c1.eq(self.s1)
m2 = Module()
m2.d.comb += self.c2.eq(self.s2)
m2.d.sync += self.c3.eq(self.s3)
m1.submodules.foo = m2
f1 = m1.elaborate(platform=None)
self.assertRepr(f1.statements, """
(
(eq (sig c1) (sig s1))
)
""")
self.assertEqual(f1.drivers, {
None: SignalSet((self.c1,))
})
self.assertEqual(len(f1.subfragments), 1)
(f2, f2_name), = f1.subfragments
self.assertEqual(f2_name, "foo")
self.assertRepr(f2.statements, """
(
(eq (sig c2) (sig s2))
(eq (sig c3) (sig s3))
)
""")
self.assertEqual(f2.drivers, {
None: SignalSet((self.c2,)),
"sync": SignalSet((self.c3,))
})
self.assertEqual(len(f2.subfragments), 0)