From e62ae04613edd4c41367c3b88dd71dc2ecd49837 Mon Sep 17 00:00:00 2001 From: Jeremy Lau <30300826+fdxmw@users.noreply.github.com> Date: Fri, 17 Jul 2026 19:45:28 -0700 Subject: [PATCH] Fix a `CompiledSimulation` `concat` bug. The bug occurs when the first arg to `concat` crosses a 64-bit limb boundary. The checks in `_build_concat` were reversed: We must check if the current arg has more bits before checking if we're done with the `concat`. Add a new `test_concat_split_offset_start` test that fails without this fix. --- pyrtl/compilesim.py | 4 +- tests/test_compilesim.py | 95 ++++++++++++++++++++++++++++++++++------ 2 files changed, 83 insertions(+), 16 deletions(-) diff --git a/pyrtl/compilesim.py b/pyrtl/compilesim.py index 541a69f5..e7022dfc 100644 --- a/pyrtl/compilesim.py +++ b/pyrtl/compilesim.py @@ -662,11 +662,11 @@ def _build_concat(self, write, _op, _param, args, dest): arg, alimb, astart, asize = curr res.append(f"(({arg}[{alimb}]>>{astart})<<{dpos})") dpos += asize - if dpos >= dest.bitwidth - 64 * n: - break if dpos > 64: curr = (arg, alimb, 64 - (dpos - asize), dpos - 64) break + if dpos >= dest.bitwidth - 64 * n: + break curr = next(pieces) if dpos == 64: break diff --git a/tests/test_compilesim.py b/tests/test_compilesim.py index 6f77acf2..8f967ff9 100644 --- a/tests/test_compilesim.py +++ b/tests/test_compilesim.py @@ -123,20 +123,6 @@ def test_bitslice2_and_concat_simulation(self): self.r.next <<= pyrtl.concat(left, right) self.check_trace("o 01377777\n") - def test_multiple_limb_bitslice_simulation(self): - pyrtl.reset_working_block() - - # `big_input`'s storage spans two 64-bit limbs. - big_input = pyrtl.Input(name="big_input", bitwidth=128) - - out = pyrtl.Output(name="out", bitwidth=32) - # This slice must fetch and combine bits from both of `big_input`'s limbs. - out <<= big_input[48:80] - - sim = self.sim() - sim.step({"big_input": 0xFFFF_EEEE_DDDD_1234_5678_CCCC_BBBB_AAAA}) - self.assertEqual(sim.inspect("out"), 0x1234_5678) - def test_sparse_bitslice_simulation(self): pyrtl.reset_working_block() @@ -180,6 +166,87 @@ def test_equals_simulation(self): self.check_trace("o 01010101\n") +class MultiLimbTestsBase(unittest.TestCase): + """Test operations that read multiple arg limbs, and write multiple dest limbs.""" + + def setUp(self): + pyrtl.reset_working_block() + + def test_multiple_limb_bitslice(self): + # `big_input`'s storage spans two 64-bit limbs. + big_input = pyrtl.Input(name="big_input", bitwidth=128) + + out = pyrtl.Output(name="out", bitwidth=32) + # This slice must fetch and combine bits from both of `big_input`'s limbs. + out <<= big_input[48:80] + + sim = self.sim() + sim.step({"big_input": 0xFFFF_EEEE_DDDD_1234_5678_CCCC_BBBB_AAAA}) + self.assertEqual(sim.inspect("out"), 0x1234_5678) + + def test_concat_fully_aligned(self): + """Test concat where 64-bit args align with the dest limbs.""" + a = pyrtl.Input(name="a", bitwidth=64) + b = pyrtl.Input(name="b", bitwidth=64) + + concat = pyrtl.Output(name="concat", bitwidth=128) + concat <<= pyrtl.concat(a, b) + + sim = self.sim() + sim.step({"a": 0x8888_9999_AAAA_BBBB, "b": 0xCCCC_DDDD_EEEE_FFFF}) + self.assertEqual( + sim.inspect("concat"), 0x8888_9999_AAAA_BBBB_CCCC_DDDD_EEEE_FFFF + ) + + def test_concat_incrementally_aligned(self): + """Test concat where 32-bit args align with the 64-bit dest limbs.""" + a = pyrtl.Input(name="a", bitwidth=32) + b = pyrtl.Input(name="b", bitwidth=32) + c = pyrtl.Input(name="c", bitwidth=32) + d = pyrtl.Input(name="d", bitwidth=32) + + concat = pyrtl.Output(name="concat", bitwidth=128) + concat <<= pyrtl.concat(a, b, c, d) + + sim = self.sim() + sim.step( + {"a": 0x8888_9999, "b": 0xAAAA_BBBB, "c": 0xCCCC_DDDD, "d": 0xEEEE_FFFF} + ) + self.assertEqual( + sim.inspect("concat"), 0x8888_9999_AAAA_BBBB_CCCC_DDDD_EEEE_FFFF + ) + + def test_concat_split_aligned_start(self): + """Test concat where the last arg must be split across two dest limbs, and the + low dest limb's bits all come from the last arg. + """ + a = pyrtl.Input(name="a", bitwidth=16) + b = pyrtl.Input(name="b", bitwidth=16) + c = pyrtl.Input(name="c", bitwidth=80) + + concat = pyrtl.Output(name="concat", bitwidth=112) + concat <<= pyrtl.concat(a, b, c) + + sim = self.sim() + sim.step({"a": 0xAAAA, "b": 0xBBBB, "c": 0xCCCC_DDDD_EEEE_FFFF_0000}) + self.assertEqual(sim.inspect("concat"), 0xAAAA_BBBB_CCCC_DDDD_EEEE_FFFF_0000) + + def test_concat_split_offset_start(self): + """Test concat where the first arg must be split across two dest limbs, and the + low dest limb's bits are merged from multiple args. + """ + a = pyrtl.Input(name="a", bitwidth=48) + b = pyrtl.Input(name="b", bitwidth=16) + c = pyrtl.Input(name="c", bitwidth=16) + + concat = pyrtl.Output(name="concat", bitwidth=80) + concat <<= pyrtl.concat(a, b, c) + + sim = self.sim() + sim.step({"a": 0xAAAA_BBBB_CCCC, "b": 0xDDDD, "c": 0xEEEE}) + self.assertEqual(sim.inspect("concat"), 0xAAAA_BBBB_CCCC_DDDD_EEEE) + + class PrintTraceBase(unittest.TestCase): # note: doesn't include tests for compact=True because all the tests test that def setUp(self):