From 35bd90337126d9a891ea2ebe613529c492df9d25 Mon Sep 17 00:00:00 2001 From: Dave Keeshan <96727608+davekeeshan@users.noreply.github.com> Date: Fri, 3 Feb 2023 17:08:02 +0000 Subject: [PATCH] True case statment mapping support (#408) * Add initial support for match/case in python, only available in 3.10 on * Fixed testing so that only the match code is used on python 3.10 or above * Add spport for enumerated types in match statments * Rewrote the code to leverage the correct AST way of walking the case/match tree to build the correct RTL versions * Removed enumerate where not used * Removed enumerate where not used * Move appropriate code into match_case definition * Remove unused defs * Remove unused defs * Cleanup of redundant code, and replace some simple variable names with more representative names --- Makefile | 14 +- myhdl/conversion/_toVHDL.py | 72 ++++ myhdl/conversion/_toVerilog.py | 66 ++++ .../conversion/general/test_match_py310.py | 366 ++++++++++++++++++ requirements.txt | 1 + 5 files changed, 515 insertions(+), 4 deletions(-) create mode 100644 myhdl/test/conversion/general/test_match_py310.py diff --git a/Makefile b/Makefile index 2bc5703b..85205786 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,12 @@ ANSI_GREEN=`tput setaf 2` ANSI_CYAN=`tput setaf 6` ANSI_RESET=`tput sgr0` +# Some tests contain python 3.10 syntax, they can even be presented to pytest to parse with the wrong python +PYV=$(shell python -c "import sys;t='{v[0]}{v[1]:02}'.format(v=list(sys.version_info[:2]));sys.stdout.write(t)") +ifeq ($(shell test $(PYV) -lt 310; echo $$?),0) + PYTEST_OPTS += --ignore-glob='*_py310.py' +endif + install: python setup.py install @@ -33,7 +39,7 @@ release: git push && git push --tags clean: - rm -rf *.vhd *.v *.o *.log *.hex work/ cosimulation/icarus/myhdl.vpi + rm -rf *.vhd *.v *.o *.log *.vcd *.hex work/ cosimulation/icarus/myhdl.vpi lint: pyflakes myhdl/ @@ -42,7 +48,7 @@ black: black myhdl/ core: @echo -e "\n${ANSI_CYAN}running test: $@ ${ANSI_RESET}" - pytest ./myhdl/test/core ${PYTEST_OPTS} + pytest -v ./myhdl/test/core ${PYTEST_OPTS} iverilog_myhdl.vpi: ${MAKE} -C cosimulation/icarus myhdl.vpi @@ -61,7 +67,7 @@ iverilog_bugs: iverilog: iverilog_cosim @echo -e "\n${ANSI_CYAN}running test: $@ ${ANSI_RESET}" - pytest ./myhdl/test/conversion/general ./myhdl/test/conversion/toVerilog ./myhdl/test/bugs --sim iverilog ${PYTEST_OPTS} + pytest -v ./myhdl/test/conversion/general ./myhdl/test/conversion/toVerilog ./myhdl/test/bugs --sim iverilog ${PYTEST_OPTS} ghdl_general: pytest ./myhdl/test/conversion/general --sim ghdl ${PYTEST_OPTS} @@ -74,6 +80,6 @@ ghdl_bugs: ghdl: @echo -e "\n${ANSI_CYAN}running test: $@ ${ANSI_RESET}" - pytest ./myhdl/test/conversion/general ./myhdl/test/conversion/toVHDL ./myhdl/test/bugs --sim ghdl ${PYTEST_OPTS} + pytest -v ./myhdl/test/conversion/general ./myhdl/test/conversion/toVHDL ./myhdl/test/bugs --sim ghdl ${PYTEST_OPTS} pytest: core iverilog ghdl \ No newline at end of file diff --git a/myhdl/conversion/_toVHDL.py b/myhdl/conversion/_toVHDL.py index 80c18499..031e5e6c 100644 --- a/myhdl/conversion/_toVHDL.py +++ b/myhdl/conversion/_toVHDL.py @@ -1441,6 +1441,78 @@ class _ConvertVisitor(ast.NodeVisitor, _ConversionMixin): self.mapToCase(node) else: self.mapToIf(node) + + def visit_Match(self, node): + self.write("case ") + self.visit(node.subject) + self.write(" is") + self.indent() + for case in node.cases: + case.subject = node.subject + self.visit(case) + + self.dedent() + self.writeline() + self.write("end case;") + + def visit_match_case(self, node): + self.writeline() + self.write("when ") + + pattern = node.pattern + pattern.subject = node.subject + self.visit(pattern) + + self.write(" => ") + self.indent() + # Write all the multiple assignment per case + for stmt in node.body: + self.writeline() + self.visit(stmt) + self.dedent() + + def visit_MatchValue(self, node): + baseobj = self.getObj(node.subject) + item = node.value + obj = self.getObj(item) + + if isinstance(obj, EnumItemType): + itemRepr = obj._toVHDL() + elif hasattr(baseobj, '_nrbits'): + itemRepr = self.BitRepr(item.value, baseobj) + else: + raise AssertionError("Unknown type %s " % (type(obj))) + self.write(itemRepr) + + def visit_MatchSingleton(self, node): + raise AssertionError("Unsupported Match type %s " % (type(node))) + + def visit_MatchSequence(self, node): + raise AssertionError("Unsupported Match type %s " % (type(node))) + + def visit_MatchStar(self, node): + raise AssertionError("Unsupported Match type %s " % (type(node))) + + def visit_MatchMapping(self, node): + raise AssertionError("Unsupported Match type %s " % (type(node))) + + def visit_MatchClass(self, node): + for pattern in node.patterns: + pattern.subject = node.subject + self.visit(pattern) + + def visit_MatchAs(self, node): + if node.name is None and node.pattern is None: + self.write("others") + else: + raise AssertionError("Unknown name %s or pattern %s" % (node.name, node.pattern)) + + def visit_MatchOr(self, node): + for i, pattern in enumerate(node.patterns): + pattern.subject = node.subject + self.visit(pattern) + if not i == len(node.patterns)-1: + self.write(" | ") def mapToCase(self, node): var = node.caseVar diff --git a/myhdl/conversion/_toVerilog.py b/myhdl/conversion/_toVerilog.py index 3088434d..227c9469 100644 --- a/myhdl/conversion/_toVerilog.py +++ b/myhdl/conversion/_toVerilog.py @@ -1041,6 +1041,72 @@ class _ConvertVisitor(ast.NodeVisitor, _ConversionMixin): else: self.mapToIf(node) + def visit_Match(self, node): + self.write("case (") + self.visit(node.subject) + self.write(")") + self.indent() + for case in node.cases: + self.visit(case) + self.writeline() + + self.dedent() + self.writeline() + self.write("endcase") + + def visit_match_case(self, node): + pattern = node.pattern + self.visit(pattern) + + self.write(": begin ") + self.indent() + # Write all the multiple assignment per case + for stmt in node.body: + self.writeline() + self.visit(stmt) + self.dedent() + self.writeline() + self.write("end") + + def visit_MatchValue(self, node): + item = node.value + obj = self.getObj(item) + + if isinstance(obj, EnumItemType): + itemRepr = obj._toVerilog() + else: + itemRepr = self.IntRepr(item.value, radix='hex') + + self.write(itemRepr) + + def visit_MatchSingleton(self, node): + raise AssertionError("Unsupported Match type %s " % (type(node))) + + def visit_MatchSequence(self, node): + raise AssertionError("Unsupported Match type %s " % (type(node))) + + def visit_MatchStar(self, node): + raise AssertionError("Unsupported Match type %s " % (type(node))) + + def visit_MatchMapping(self, node): + raise AssertionError("Unsupported Match type %s " % (type(node))) + + def visit_MatchClass(self, node): + for pattern in node.patterns: + self.visit(pattern) + + def visit_MatchAs(self, node): + if node.name is None and node.pattern is None: + self.write("default") + else: + raise AssertionError("Unknown name %s or pattern %s" % (node.name, node.pattern)) + + def visit_MatchOr(self, node): + for i, pattern in enumerate(node.patterns): + self.visit(pattern) + if not i == len(node.patterns)-1: + self.write(" | ") + def mapToCase(self, node, *args): var = node.caseVar # self.write("// synthesis parallel_case") diff --git a/myhdl/test/conversion/general/test_match_py310.py b/myhdl/test/conversion/general/test_match_py310.py new file mode 100644 index 00000000..2ea08379 --- /dev/null +++ b/myhdl/test/conversion/general/test_match_py310.py @@ -0,0 +1,366 @@ +import sys +from myhdl import * +import pytest + +#SELOPTS = enum('SEL0', 'SEL1', 'SEL2', 'SEL3') + +@block +def mux4a( + sel, + in0, + in1, + in2, + in3, + out0 +): + + @always_comb + def rtl(): + if sel == 0: + out0.next = in0 + elif sel == 1: + out0.next = in1 + elif sel == 2: + out0.next = in2 + else: + out0.next = in3 + + return instances() + +@block +def mux4b(sel, in0, in1, in2, in3, out0): + @always_comb + def rtl(): + match sel: + case 0: + out0.next = in0 + case 1: + out0.next = in1 + case 2: + out0.next = in2 + case _: + out0.next = in3 + return instances() + +@block +def mux4c(sel, in0, in1, in3, out0): + @always_comb + def rtl(): + match sel: + case 0: + out0.next = in0 + case 1 | 2: + out0.next = in1 + case _: + out0.next = in3 + return instances() + +t_opts = enum('SEL0', 'SEL1', 'SEL2', 'SEL3') + +@block +def enumMux4a( + sel, + in0, + in1, + in2, + in3, + out0, +): + + sel_enum = Signal(t_opts.SEL0) + + @always_comb + def mapping(): + if 0 == sel: + sel_enum.next = t_opts.SEL0 + elif 1 == sel: + sel_enum.next = t_opts.SEL1 + elif 2 == sel: + sel_enum.next = t_opts.SEL2 + elif 3 == sel: + sel_enum.next = t_opts.SEL3 + + + @always_comb + def rtl(): + if sel_enum == t_opts.SEL0: + out0.next = in0 + elif sel_enum == t_opts.SEL1: + out0.next = in1 + elif sel_enum == t_opts.SEL2: + out0.next = in2 + elif sel_enum == t_opts.SEL3: + out0.next = in3 + + return instances() + +@block +def enumMux4b( + sel, + in0, + in1, + in2, + in3, + out0, +): + sel_enum = Signal(t_opts.SEL0) + + @always_comb + def mapping(): + if 0 == sel: + sel_enum.next = t_opts.SEL0 + elif 1 == sel: + sel_enum.next = t_opts.SEL1 + elif 2 == sel: + sel_enum.next = t_opts.SEL2 + elif 3 == sel: + sel_enum.next = t_opts.SEL3 + + + @always_comb + def rtl(): + match sel_enum: + case t_opts.SEL0: + out0.next = in0 + case t_opts.SEL1: + out0.next = in1 + case t_opts.SEL2: + out0.next = in2 + case _: + out0.next = in3 + + return instances() + +t_fsma_opts = enum('IDLE', 'READ', 'WRITE', 'ERROR') + +@block +def fsm4a( + clk, + rst, + rd, + wr, +): + smp = Signal(t_fsma_opts.IDLE) + + @always(clk.posedge) + def rtl(): + + if rst: + smp.next = t_fsma_opts.IDLE + else: + match smp: + case t_fsma_opts.IDLE: + smp.next = t_fsma_opts.IDLE + if rd: + smp.next = t_fsma_opts.READ + if wr: + smp.next = t_fsma_opts.WRITE + case t_fsma_opts.READ: + smp.next = t_fsma_opts.IDLE + case t_fsma_opts.WRITE: + smp.next = t_fsma_opts.IDLE + case _: + smp.next = t_fsma_opts.IDLE + + return instances() + +@block +def fsm4b( + clk, + a, + b, + c, + z, +): + + @always(clk.posedge) + def rtl(): + match concat(a,b,c): + case intbv(0b111) : + z.next = 0x6 + case intbv(0b101) | intbv(0b110) : + z.next = 0x7 + case _: + z.next = 0x0 + + return instances() + + + +@block +def muxBench0(setup=0): + + clk = Signal(bool(0)) + sel = Signal(intbv(0)[2:]) + in0 = Signal(intbv(0)[4:]) + in1 = Signal(intbv(0)[4:]) + in2 = Signal(intbv(0)[4:]) + in3 = Signal(intbv(0)[4:]) + out0 = Signal(intbv(0)[4:]) + + @instance + def clkgen(): + clk.next = 1 + for i in range(400): + yield delay(10) + clk.next = not clk + + @instance + def stimulus(): + sel.next = 0x0 + in0.next = 0xa + in1.next = 0xb + in2.next = 0xc + in3.next = 0xd + yield clk.posedge + yield clk.posedge + sel.next = 0x1 + yield clk.posedge + sel.next = 0x2 + yield clk.posedge + sel.next = 0x3 + yield clk.posedge + sel.next = 0x0 + yield clk.posedge + + raise StopSimulation + + @instance + def check(): + yield clk.posedge + yield clk.posedge + assert out0 == 0xa + yield clk.posedge + assert out0 == 0xb + yield clk.posedge + assert out0 == 0xc + yield clk.posedge + assert out0 == 0xd + yield clk.posedge + assert out0 == 0xa + yield clk.posedge + + if 0 == setup: + i_mux = mux4a(sel, in0, in1, in2, in3, out0) + elif 1 == setup: + i_mux = mux4b(sel, in0, in1, in2, in3, out0) + elif 2 == setup: + i_mux = enumMux4a(sel, in0, in1, in2, in3, out0) + else: + i_mux = enumMux4b(sel, in0, in1, in2, in3, out0) + + return instances() + +def test_mux4a_convert(): + clk = Signal(bool(0)) + sel = Signal(intbv(0)[2:]) + in0 = Signal(intbv(0)[4:]) + in1 = Signal(intbv(0)[4:]) + in2 = Signal(intbv(0)[4:]) + in3 = Signal(intbv(0)[4:]) + out0 = Signal(intbv(0)[4:]) + + i_dut = mux4a(sel, in0, in1, in2, in3, out0) + assert i_dut.analyze_convert() == 0 + +def test_muxBench0(): + sim = muxBench0(0) + sim.run_sim() + +def test_muxBench0_convert(): + i_dut = muxBench0(0) + assert i_dut.analyze_convert() == 0 + +#@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher") +def test_mux4b_convert(): + clk = Signal(bool(0)) + sel = Signal(intbv(0)[2:]) + in0 = Signal(intbv(0)[4:]) + in1 = Signal(intbv(0)[4:]) + in2 = Signal(intbv(0)[4:]) + in3 = Signal(intbv(0)[4:]) + out0 = Signal(intbv(0)[4:]) + + i_dut = mux4b(sel, in0, in1, in2, in3, out0) + assert i_dut.analyze_convert() == 0 + +#@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher") +def test_mux4b_convert(): + clk = Signal(bool(0)) + sel = Signal(intbv(0)[2:]) + in0 = Signal(intbv(0)[4:]) + in1 = Signal(intbv(0)[4:]) + in3 = Signal(intbv(0)[4:]) + out0 = Signal(intbv(0)[4:]) + + i_dut = mux4c(sel, in0, in1, in3, out0) + assert i_dut.analyze_convert() == 0 + +#@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher") +def test_muxBench1(): + sim = muxBench0(1) + sim.run_sim() + +#@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher") +def test_muxBench1_convert(): + i_dut = muxBench0(1) + assert i_dut.analyze_convert() == 0 + +def test_muxBench2(): + sim = muxBench0(2) + sim.run_sim() + +def test_muxBench2_convert(): + i_dut = muxBench0(2) + assert i_dut.analyze_convert() == 0 + +def test_enumMux4a_convert(): + clk = Signal(bool(0)) + sel = Signal(intbv(0)[2:]) + in0 = Signal(intbv(0)[4:]) + in1 = Signal(intbv(0)[4:]) + in2 = Signal(intbv(0)[4:]) + in3 = Signal(intbv(0)[4:]) + out0 = Signal(intbv(0)[4:]) + + i_dut = enumMux4a(sel, in0, in1, in2, in3, out0) + assert i_dut.analyze_convert() == 0 + +def test_muxBench3(): + sim = muxBench0(3) + sim.run_sim() + +def test_enumMux4b_convert(): + clk = Signal(bool(0)) + sel = Signal(intbv(0)[2:]) + in0 = Signal(intbv(0)[4:]) + in1 = Signal(intbv(0)[4:]) + in2 = Signal(intbv(0)[4:]) + in3 = Signal(intbv(0)[4:]) + out0 = Signal(intbv(0)[4:]) + + i_dut = enumMux4b(sel, in0, in1, in2, in3, out0) + assert i_dut.analyze_convert() == 0 + + +def test_fsm4a_convert(): + clk = Signal(bool(0)) + rst = Signal(intbv(0)[1:]) + rd = Signal(intbv(0)[1:]) + wr = Signal(intbv(0)[1:]) + + + i_dut = fsm4a(clk, rst, rd, wr) + assert i_dut.analyze_convert() == 0 + +def test_fsm4b_convert(): + clk = Signal(bool(0)) + a = Signal(intbv(0)[1:]) + b = Signal(intbv(0)[1:]) + c = Signal(intbv(0)[1:]) + z = Signal(intbv(0)[3:]) + + + i_dut = fsm4b(clk, a, b, c, z) + assert i_dut.analyze_convert() == 0 + diff --git a/requirements.txt b/requirements.txt index 0195f1ff..04bd0d96 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ pytest +pytest-xdist pyflakes black