diff --git a/test/lib/test_transformers.py b/test/lib/test_transformers.py index 3e4a647..22b892c 100644 --- a/test/lib/test_transformers.py +++ b/test/lib/test_transformers.py @@ -5,6 +5,7 @@ from transactron import * from transactron.lib.adapters import Adapter, AdapterTrans from transactron.lib.transformers import * +from transactron.testing.testbenchio import CallTrigger from transactron.utils._typing import ModuleLike, MethodStruct, RecordDict from transactron.utils import ModuleConnector from transactron.testing import ( @@ -294,3 +295,72 @@ def elaborate(self, platform): m.submodules.method = self.method = TestbenchIO(AdapterTrans(product.use(m))) return m + + +class NonexclusiveWrapperTestCircuit(Elaboratable): + def __init__(self, iosize: int, wrappers: int, callers: int): + self.iosize = iosize + self.wrappers = wrappers + self.callers = callers + self.sources: list[list[TestbenchIO]] = [] + + def elaborate(self, platform): + m = TModule() + + layout = data_layout(self.iosize) + + m.submodules.target = self.target = TestbenchIO(Adapter.create(i=layout, o=layout)) + + for i in range(self.wrappers): + nonex = NonexclusiveWrapper(self.target.adapter.iface).use(m) + sources = [] + self.sources.append(sources) + + for j in range(self.callers): + m.submodules[f"source_{i}_{j}"] = source = TestbenchIO(AdapterTrans(nonex)) + sources.append(source) + + return m + + +class TestNonexclusiveWrapper(TestCaseWithSimulator): + def test_nonexclusive_wrapper(self): + iosize = 4 + wrappers = 2 + callers = 2 + iterations = 100 + m = NonexclusiveWrapperTestCircuit(iosize, wrappers, callers) + + def caller_process(i: int): + async def process(sim: TestbenchContext): + for _ in range(iterations): + j = random.randrange(callers) + data = random.randrange(2**iosize) + ret = await m.sources[i][j].call(sim, data=data) + assert ret.data == (data + 1) % (2**iosize) + await self.random_wait_geom(sim, 0.5) + + return process + + @def_method_mock(lambda: m.target) + def target(data): + return {"data": data + 1} + + with self.run_simulation(m) as sim: + self.add_mock(sim, target()) + for i in range(wrappers): + sim.add_testbench(caller_process(i)) + + def test_no_conflict(self): + m = NonexclusiveWrapperTestCircuit(1, 1, 2) + + async def process(sim: TestbenchContext): + res1, res2 = await CallTrigger(sim).call(m.sources[0][0], data=1).call(m.sources[0][1], data=2).until_done() + assert res1 is not None and res2 is not None # there was no conflict, however the result is undefined + + @def_method_mock(lambda: m.target) + def target(data): + return {"data": data + 1} + + with self.run_simulation(m) as sim: + sim.add_testbench(process) diff --git a/transactron/lib/transformers.py b/transactron/lib/transformers.py index 0a75cef..cf374f2 100644 --- a/transactron/lib/transformers.py +++ b/transactron/lib/transformers.py @@ -28,6 +28,7 @@ "Collector", "CatTrans", "ConnectAndMapTrans", + "NonexclusiveWrapper", ] @@ -453,3 +454,27 @@ def elaborate(self, platform): m.submodules.connect = ConnectTrans(self.method1, transformer.method) return m + + +class NonexclusiveWrapper(Elaboratable, Transformer): + """Nonexclusive wrapper around a method. + + Useful when you can assume, for external reasons, that a given method will + never be called more than once in a given clock cycle - even when the + call graph indicates it could. + + Possible use case is unifying parallel pipelines with the same latency. + """ + + def __init__(self, target: Method): + self.target = target + self.method = Method.like(target) + + def elaborate(self, platform): + m = TModule() + + @def_method(m, self.method, nonexclusive=True) + def _(arg): + return self.target(m, arg) + + return m